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

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

## 結論

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

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

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

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

```dart
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);` とやるんだな。

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

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

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

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

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

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

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

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

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

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

とのこと。

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

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

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

## `PopupRoute` と `PageRoute` の違い

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

PageRoute
> A modal route that replaces the entire screen.

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

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

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

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

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

```dart
  @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.

上のコードに出てくる `Listener` と `AbsorbPointer`、`FocusScope` についても調べないと。

## 続く

## リンク

[showDialog<T> function](https://docs.flutter.io/flutter/material/showDialog.html)