所感箱

所感を書きためていきます

Flutterで開発で利用しているGetXパッケージの備忘録

今回はGetXパッケージについて簡単な説明と内部実装を追った内容を残しておく。

pub.dev

GetXは何か?

GetXは状態管理、依存性注入、ルーティングの機能を備えたパッケージで、GitHubのスター数も4000を超えている。特にルーティング機能を利用して、画面遷移を非常に楽に書けるようになる印象を持ったので、簡単なサンプルを元に紹介したいと思う。

通常、Flutterで画面遷移を実装する場合下記のようになる。

class _MyList extends State<List> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ・・・
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () {
            // BuildContextが必要なため、呼び出し元が限られる
            Navigator.push(
                context,
                MaterialPageRoute(
                  settings: const RouteSettings(name: "/new"),
                  builder: (context) => InputForm(null)
                )
            );
        },
      ),
    );
  }

GetXパッケージを使って書いてみると以下のようになる。

class _MyList extends State<List> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ・・・
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () {
            // BuildContextが不要になった
            Get.to(InputForm(null))
        },
      ),
    );
  }

ちなみにルートはMaterialAppではなくGetMaterialAppを利用する必要がある

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'Flutter Demo',
      ・・・
  }
}

このようにGetXパッケージを使用することで、シンプルかつBuildContextから分離された書き方が可能になる。GetXパッケージのドキュメントでは、以下のように説明されている。

抜粋

You don't need context to access your controllers/blocs through an inheritedWidget, so you completely decouple your presentation logic and business logic from your visualization layer.

和訳

継承されたウィジェットを介してコントローラー/ブロックにアクセスするためにコンテキストは必要ないため、プレゼンテーションロジックとビジネスロジックを視覚化レイヤーから完全に分離します。

この説明通りにFlutterの制約から剥がして、設計の自由さが増すことはメリットが大きそうだと思うが、 BuildContextがない場合、副作用はないのか気になったので、内部実装を調べてみた。

どうやって動いているか?

先にそもそも標準の画面遷移はどのように行われているかを少し追ってみる。

Navigator.pushの内部は以下のようにNavigator.of(context)で必要なNavigationStateが取得し、pushで画面遷移の指示を行っている。

  @optionalTypeArgs
  static Future<T?> push<T extends Object?>(BuildContext context, Route<T> route) {
    return Navigator.of(context).push(route);
  }

これはpop、replaceなど画面遷移に関わる関数も同様にNavigator.of(context)からNavigationStateを取得し、全ての画面遷移状態を管理しているようだった。

そしてこの関数では以下のように、NavigatorStateを探索して見つかったものを返却している

  static NavigatorState of(
    BuildContext context, {
    bool rootNavigator = false,
  }) {
    NavigatorState? navigator;
    // 自分自身がNavigatorStateを持っていたら返却
    if (context is StatefulElement && context.state is NavigatorState) {
        navigator = context.state as NavigatorState;
    }
    // NavigatorStateを探索的に探している
    if (rootNavigator) {
      navigator = context.findRootAncestorStateOfType<NavigatorState>() ?? navigator;
    } else {
      navigator = navigator ?? context.findAncestorStateOfType<NavigatorState>();
    }

    ・・・

    return navigator!;
  }

BuildContextは各ウィジェットに紐づくクラスなので、渡されたBuildContetを起点に、ウィジェットツリーから、NavigatorStateを探索して、ルーティングを実行する仕組みになっているようだった。

GetXのNavigatorはどのように動いているか

ここまで見てみると、標準のルーティングがウィジェットツリーに依存しているため、GetXがBuildContextなしに画面遷移を実現する方法が気になってくる。Get.toの内部実装を見てみたところ以下のようになっていた。

  // Get.to() 関数
  Future<T?>? to<T>(
    dynamic page, {
    bool? opaque,
    Transition? transition,
    Curve? curve,
    Duration? duration,
    int? id,
    String? routeName,
    bool fullscreenDialog = false,
    dynamic arguments,
    Bindings? binding,
    bool preventDuplicates = true,
    bool? popGesture,
    double Function(BuildContext context)? gestureWidth,
  }) {
    ・・・
    // idはデフォルトはnull
    return global(id).currentState?.push<T>(
        ・・・
    );
  }

Get.to内部ではglobal関数からGlobalKeyが返却されていて、どうやらGetパッケージでは独自にNavigatorStateを管理しBuildContextの依存なしでルーティングを実現しているようだった。 もう少し詳しく実装を見ていくと以下の通り。

extension GetNavigation on GetInterface {
    // 指定した引数に応じてたをGlobalKey<NavigatorState>返却する*
    GlobalKey<NavigatorState> global(int? k) {
      GlobalKey<NavigatorState> _key;
      if (k == null) {
        _key = key;
      } else {
        if (!keys.containsKey(k)) {
          throw 'Route id ($k) not found';
        }
        _key = keys[k]!;
      }

      ・・・

      return _key;
    }

    ・・・
    // このGlobalKey<NavigatorState>経由で画面遷移時にNavigatorStateにアクセスする
    GlobalKey<NavigatorState> get key => _getxController.key;
    static GetMaterialController _getxController = GetMaterialController();
}

このGlobalKeyはget_main.dartに定義された変数でアクセスでき、GetMaterialAppのbuild時にnavigatorKeyに対して指定がされていた。

// get_main.dart
class _GetImpl extends GetInterface {}
final Get = _GetImpl();
// get_material_app.dart
MaterialApp(
    ・・・
    navigatorKey:
         (navigatorKey == null ? Get.key : Get.addKey(navigatorKey!))
    ・・・
)

GetXではこのようにBuildContextの依存せず、画面遷移を実現しているようだった。 これに対して、標準のMaterialAppでは、widgetsAppのnavigatorKeyを利用しているようだった。

    return WidgetsApp(
    ・・・
      navigatorKey: widget.navigatorKey,
    ・・・
   )

簡単にまとめると以下のような違いとなりそう

  • GetXではGlobalKeyを直接参照して、NavigatorStateを管理/参照する
  • 標準実装ではBuildContextを経由し、ウイジェットツリーから、NavigatorStateを管理/参照する

まとめ

他の機能をあまり利用しておらず、紹介できなかったが、画面遷移がシンプルに書けるようになるのが良い感じなので、ルーティングだけに閉じて利用するのもありのように思う。