LoginSignup
9
4

More than 5 years have passed since last update.

Flutter における Dialog の内部実装に関する雑なノート

Last updated at Posted at 2018-09-07

Flutter では showDialog というグローバル関数で簡単にダイアログを表示することができる。画面にオーバーレイして表示されるダイアログのような独自のUIコンポーネントを作るにはどうしたらいいんだろう?と思って showDialog の中身をざっくり見てみた。

結論

結論から言うと、PopupRoute という抽象クラスを継承して独自のオーバーレイ Route を作ることができる。

Flutter のダイアログはどのように実装されているんだろうか?

showDialog は内部的に showGeneralDialog を呼び出している。showGeneralDialogpageBuilder という引数で渡された builder を元にダイアログを作る。builder にはダイアログに表示したいなんらかの Widget を返す関数を指定する。

showGeneralDialog の実装は実質これだけ:

return Navigator.of(context, rootNavigator: true).push(new _DialogRoute<T>(
  pageBuilder: pageBuilder,
  barrierDismissible: barrierDismissible,
  barrierLabel: barrierLabel,
  barrierColor: barrierColor,
  transitionDuration: transitionDuration,
  transitionBuilder: transitionBuilder,
));

引数として渡された context を元に Navigator を取得し、その Navigator_DialogRoute というものを push している。こうやってダイアログの表示も Navigator が一種のルーティングとして処理しているのか。だからダイアログを閉じる時に Navigator.pop(context); とやるんだな。

_DialogRoutePopupRoute という抽象クラスを継承したプライベートクラス。継承ツリーは最終的に Route という抽象クラスに行き着く。

_DialogRoute -> PopupRoute -> ModalRoute -> [TransitionRoute, LocalHistoryRoute] -> OverlayRoute -> Route

ポップアップメニューなんかも ModalRoute を使って実装されているっぽい。

通常の画面遷移のための Route はどのように実現されているんだろうか?

通常の画面遷移には MaterialPageRoute というクラスが使われる。MaterialApp というアプリケーションの根っこのところに配置するコンポーネントの初期化のときに routes という引数でルーティングの設定を渡せるんだけど、

MaterialApp(
  title: 'Flutter Demo',
  theme: theme(context),
  initialRoute: '/',
  routes: {
    '/': (context) => FirstScreen(),
    '/signup': (context) => SignUpScreen(),
    '/login': (context) => LoginScreen(),
  },
);

これらは最終的に MaterialPageRoute として Navigator に処理される。

Route<dynamic> _onGenerateRoute(RouteSettings settings) {
  ...
  return new MaterialPageRoute<dynamic>(
    builder: builder,
    settings: settings,
  );
  ...
}

WidgetsApp クラスが内部で Navigator を初期していて、そのときに onGenerateRoute として routes を作る Factory (RouteFactory)が渡される。WidgetsApp というのは MaterialAppCupertinoApp がその build メソッドで返すウィジェットで、

A convenience class that wraps a number of widgets that are commonly required for an application.

とのこと。

MaterialPageRoutePageRoute のサブクラスで、さらに継承ツリーをたどると ModalRoutePopupRoute と共通の親クラスだということがわかる。以下のような感じ:

_DialogRoute -> PopupRoute -> ModalRoute -> ...

MaterialPageRoute -> PageRoute -> ModalRoute -> ...

PopupRoutePageRoute の違い

PopupRoute

A modal route that overlays a widget over the current route.

PageRoute

A modal route that replaces the entire screen.

PopupRoutePageRoute の実装上の一番大きな違いは opaque というプロパティの値の違い。PopupRoute の実装の一部をみて見ると、

abstract class PopupRoute<T> extends ModalRoute<T> {
  ...
  @override
  bool get opaque => false;

opaque が false を返すようになっている。それだけで半透明のスクリーンを表示できるのか。

opaqueTransitionRoute で定義されているプロパティで、このフラグをもとに overlayEntries というおそらくページの各レイヤーを保持しているっぽい配列の最初の要素の opaque フラグを設定している。overlayEntriesOverlayRoute のプロパティで、OverlayEntry の配列。OverlayEntrybuilder というプロパティを持っていることから推測できるようにやっぱりページの各レイヤーを作る為のものなんだろう。(後で出てくるけど Overlay ウィジェットで使われるデータらしい)

Navigator#build は以下のような実装になっている:

  @override
  Widget build(BuildContext context) {
    assert(!_debugLocked);
    assert(_history.isNotEmpty);
    return new Listener(
      onPointerDown: _handlePointerDown,
      onPointerUp: _handlePointerUpOrCancel,
      onPointerCancel: _handlePointerUpOrCancel,
      child: new AbsorbPointer(
        absorbing: false, // it's mutated directly by _cancelActivePointers above
        child: new FocusScope(
          node: focusScopeNode,
          autofocus: true,
          child: new Overlay(
            key: _overlayKey,
            initialEntries: _initialOverlayEntries,
          ),
        ),
      ),
    );
  }

Overlay() の引数として渡される _initialOverlayEntries には Navigator_history が保持している全ての route から取得した overlayEntries が全て入っている。つまり route として表現されるダイアログやページは最終的には Overlay に Widget として積み上げられたものなんだな。まあ Flutter において全てのグラフィカル要素は Widget なのでそうなるよな。

ところで Overlay とは:

A [Stack] of entries that can be managed independently.

Overlays let independent child widgets "float" visual elements on top of
other widgets by inserting them into the overlay's [Stack]. The overlay lets
each of these widgets manage their participation in the overlay using
[OverlayEntry] objects.

上のコードに出てくる ListenerAbsorbPointerFocusScope についても調べないと。

続く

リンク

showDialog function

9
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
4