🍗

Flutter 2.5リリース周りのAPIを研究してみる

2021/10/05に公開

Dashatar生成画像
Dashatarで作成したDASHくん🍗 https://dashatar-dev.web.app/

この記事ではFlutter 2.5リリースを振り返りつつ、新しく導入された、もしくは部分的・派生的アップデートのあったAPIを取り上げます。

📃 実行可能なデモアプリを作ってみました(DartPad)
こちらのデモに沿ってポイントだけまとめていければと思います。

(コードを分割できないので見づらいです。あとChromeだとフォーカスの挙動がたまにおかしいようなのでChrome以外推奨です!)

【画面1】FloatingActionButton

FloatingActionButton

Flutter 2.5から .small と .large のコンストラクタが増えました。またそれに伴ってFloatingActionButtonThemeDataにいくつかプロパティが追加されているようです。

              FloatingActionButton.large(
                onPressed: () {},
                child: const Icon(Icons.download),
              ),
    return MaterialApp(
      theme: ThemeData(
        floatingActionButtonTheme: const FloatingActionButtonThemeData(
          extendedIconLabelSpacing: 10,
          extendedTextStyle: TextStyle(fontSize: 21),
	  extendedSizeConstraints: BoxConstraints.tightFor(height: 90),
        ),

【画面2】MaterialState.scrolledUnder

scrolledUnder

MaterialStateに新しい状態、scrolledUnderが追加されています。

Used by AppBar to indicate that the primary scrollable's content has scrolled up and behind the app bar.

とある通り、AppBarに隣接するScrollableなウィジットがスクロールしてAppBarの下に隠れた状態のことを指しています。また AppBar/SliverAppBar で使用することが前提になっているようです。試しに ScrollbarThemeData のMaterialStateに依存するプロパティでも使ってみましたが、何も変化がありませんでした。

      child: Scaffold(
        appBar: AppBar(
          backgroundColor: MaterialStateColor.resolveWith((states) {
            return states.contains(MaterialState.scrolledUnder)
                ? Colors.red
                : Theme.of(context).primaryColor;
          }),
        ),

【画面3】ScrollMetricsNotification

ScrollMetricsNotification

Notification系のクラスにScrollMetricsNotificationが加わりました。ScrollNotificationとの違いは、前者がユーザーがスクロール動作を行うごとに関連指標を通知してくれるのに対して、後者はユーザーのスクロール動作以外(リストアイテムの追加、ウィンドウサイズの変更等)のそれを通知してくれます。

デモでは試しに、ボタンを押すたびにFlutterLogoがリストに追加され、スクロール可能な領域(maxScrollExtent)が増えるたびに通知を受け取るWidgetを作ってみました。

通知を受け取るとAppBarにmaxScrollExtentの値が表示されます。

class _ScrollMetricsPageState extends State<ScrollMetricsPage> {
  int _itemCount = 4;
  double _scrollExtent = 0.0;
  String get _title => _scrollExtent == 0.0
      ? 'ScrollMetricsNotification'
      : 'maxScrollExtent: ${_scrollExtent.round()}';
 // 中略      
      child: Scaffold(
        appBar: AppBar(
          title: Text(_title),
        ),
        // ScrollNotificationだとアイテム追加しても更新されない
        body: NotificationListener<ScrollMetricsNotification>(
          onNotification: (notification) {
            setState(() {
              _scrollExtent = notification.metrics.maxScrollExtent;
            });
            return false;
          },
          child: SingleChildScrollView(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                for (var i = 0; i < _itemCount; i++)
                  const FlutterLogo(size: 120),
              ],
            ),
          ),
        ),
        floatingActionButton: FloatingActionButton(
          child: const Icon(Icons.add),
          onPressed: () => setState(() => _itemCount++),
        ),
      ),

【画面4】showMaterialBanner

MaterialBannerの関連メソッド(showMaterialBannerほか)がScaffoldMessengerStateに追加されました。

showMaterialBanner

同じようなメソッドに以前からshowSnackBarがありますが、MaterialにおけるBanner
SnackBarの違いは以下の通りだそうです。

コンポーネント 優先度 ユーザーアクション
Snackbar 低い 任意。Snackbarは自動で消える
Banner 高い〜中 任意。Bannerはユーザー自身の意図で消すか、原因解決によって消す
Material公式より引用

デモではこんなふうに実装してみました。

  Widget build(BuildContext context) {
    final banner = MaterialBanner(
      padding: const EdgeInsets.all(30),
      backgroundColor: Theme.of(context).secondaryHeaderColor,
      contentTextStyle: Theme.of(context).textTheme.headline5,
      leading: Icon(Icons.info, color: Theme.of(context).errorColor),
      content: const Text('This is the new MaterialBanner.'),
      actions: [
        TextButton(
+         onPressed: ScaffoldMessenger.of(context).hideCurrentMaterialBanner,
          child: const Text('Hide'),
        ),
      ],
    );

    return OutlinedButton.icon(
      onPressed: () {
        // showだけの場合はバナーが表示のキューに追加される
        // これは試しにやってみただけで好ましくない例と思われる
        ScaffoldMessenger.of(context)
+         ..removeCurrentMaterialBanner()
+         ..showMaterialBanner(banner);
      },
      icon: const Icon(Icons.chat),
      label: const Text('Show MaterialBanner'),
      style: const ButtonStyle(splashFactory: InkSplash.splashFactory),
    );
  }

Materila公式に「Bannerはユーザー自身の意図で消すか、原因解決によって消す」とある通り、showMaterialBannerの前にremoveCurrentMaterialBannerは非推奨だと思われます。

消す系のメソッド三兄弟の違い
hideCurrent removeCurrent clearMaterialBanners
対象 表示中のバナー 表示中のバナー 表示キュー内の全バナー
動作 消す 消す 消す
アニメーション あり なし あり

【画面5】テキスト編集時のキー動作のoverride

テキスト編集時のキー動作変更

テキスト編集時のキー動作が変更できるようになりました。テキスト編集周りの仕組みを改修してFlutterの柔軟性を上げるという計画の一環?のようです。

【参考】
https://docs.google.com/document/d/1QaVIr1bbOWJGNyyNsB75sXgTDRMuxP268sLGTBNDut0/
https://flutter.dev/docs/development/ui/advanced/actions_and_shortcuts
#85381#78522#75004 ほか


デフォルトのキー動作と各プラットフォームで決められたショートカット(DefaultTextEditingShortcutsDefaultTextEditingActions)をoverrideするには、ウィジットツリーのどこかに以下のいずれかのウィジットを挟みます。

(※ Flutter 2.5で導入されたAPIというわけではないです。)

これらをたとえば、TextFieldを挟む形で使えばそのTextFieldにフォーカスが合ったときに設定内容が有効になり、MaterialAppなどの直下で使えばアプリ全体のFocusツリーで有効になります。

今回は FocusableActionDetector を使ってみました。

    // Focusableな Shortcuts + Actions + MouseRegion ウィジット
    return FocusableActionDetector(
      shortcuts: shortcuts,
      actions: actions,
      autofocus: true,
      focusNode: _focusNode,
      child: Scaffold(

shortcutsプロパティには「Shorcutsウィジット」のshortcutsプロパティと同じものを指定します。

    const shortcuts = <ShortcutActivator, Intent>{
      SingleActivator(LogicalKeyboardKey.arrowRight): ToAdjacentIntent(next: true),
      SingleActivator(LogicalKeyboardKey.arrowLeft): ToAdjacentIntent(next: false),
      SingleActivator(LogicalKeyboardKey.keyV, meta: true): ToEdgeIntent(toStart: false),
      SingleActivator(LogicalKeyboardKey.keyC, meta: true): ToEdgeIntent(toStart: true),
    };

shortcutsはShortcutActivatorをキー、Intentを値とするMapです。
ShortcutActivatorはキーの操作そのものを指し、これによりIntent(ユーザーの意図)をFlutterに伝えることができます。

ShortcutActivatorには以下の3つがあります。

  • CharacterActivator ... 特定の文字を表すときに使う
  • LogicalKeySet ... 主にキーの組み合わせを表すときに使う
  • SingleActivator ... 矢印や単独キーとcmd/opt/shift/ctrlの組み合わせを表すときに使う

Intentは抽象クラスなので、FlutterのAPIですでに用意されている〜Intent系のクラスを使うかIntentを継承したクラスを作成します。

// 隣のページに移動したいIntent
class ToAdjacentIntent extends Intent {
  final bool next;
  const ToAdjacentIntent({required this.next});
}
// PageViewの端に移動したいIntent
class ToEdgeIntent extends Intent {
  final bool toStart;
  const ToEdgeIntent({required this.toStart});
}

そしてactionsプロパティにも「Actionsウィジット」のactionsプロパティに相当するものが入ります。

    final actions = <Type, Action>{
      ToAdjacentIntent: CallbackAction<ToAdjacentIntent>(
        onInvoke: (intent) => _toAdjacent(next: intent.next),
      ),
      ToEdgeIntent: CallbackAction<ToEdgeIntent>(
        onInvoke: (intent) => _toEdge(toStart: intent.toStart),
      ),
    };

「Intentの型」をキー、Actionを値とするMapです。キー操作に応じてコールバックを呼び出したい場合は CallbackAction を使います。

shorcutsでFlutterに伝えたIntent(ユーザーの意図)を汲んで、actionsで実際の動作を実行するという流れになっています。

ActionとIntentが分かれていたりなど一見面倒に思えますが、なぜそのような実装になっているかについては最近公式に追加されたこちらの記事に詳細が書いてあります。

https://flutter.dev/docs/development/ui/advanced/actions_and_shortcuts

デモアプリでは Cmd + C を画面左端への移動、Cmd + V を画面右端への移動にoverrideしています。TextFieldだけでなく全体で有効です。

【画面6】Androidフルスクリーンモード

immersiveSticky
SystemUiMode.immersiveSticky

Androidの各種全画面モードに細かく対応したアップデート群です。

(全画面モードの違いについてはこちらがわかりやすいです)
https://developer.android.com/training/system-ui/immersive?hl=ja

  • 観賞モード(Lean back)
  • 没入モード(Immersive)
  • アプリ優先型没入モード(Sticky immersive)

※ これ以外の通常モード(ステータスバーとナビゲーションバーが固定された状態)がEdge To Edge

最近追加されたっぽいこちらの2つのメソッドだけ使ってみました。

SystemChrome.setEnabledSystemUIMode

UIのモード(SystemUiMode)を設定することができます。

deprecatedになったsetEnabledSystemUIOverlaysの代わりに使うもののようです。

            SystemChrome.setEnabledSystemUIMode(
              SystemUiMode.immersiveSticky,
            );
SystemChrome.setSystemUIChangeCallback

フルスクリーンモードにおいて、ユーザー動作などによりオーバーレイの状態に変化があったときに呼び出されるコールバックを設定することができます。

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setSystemUIChangeCallback((systemOverlaysAreVisible) async {
    print(systemOverlaysAreVisible);
  });
  runApp(const MyApp());
}

最後に

ということで2.5というよりは、ほとんどキーボードショートカットの記事になってしまいました。これについてはいずれ別の機会に研究してみたいと思います。

Discussion