🆖

GoRouterやAutoRoute等の付き合い方

2024/02/22に公開

結論

  • DeepLinkに対応するなら、現状GoRouter一択。やらないなら、どっちでもいい。
  • Flutter Web非対応、DeepLink非対応なら、これらのパッケージを採用しなくても良い。
  • Route単位でGuardできるAutoRouteの仕組みがGoRouterにあれば最AND高

そもそもGoRouterやAutoRouteの採用理由はどこか

Flutter公式もあまり言わなくなりましたけど、Navigator 2.0っていうお気持ち文章を公式が発信しておりました。

簡単に言うと、スタックで1個1個履歴を積むやり方をすると、category/men/item/143413869みたいなURLをクリックされたらパスの階層に呼応する画面階層を再現できないので、「画面階層まるごと」差し替えられるようにしないとWebの世界では非常に厳しいという話です。

ネイティブアプリには、URLを入れるアドレスバーなんてものはありませんので、スタックで1個1個積んで取れば良い。Webはユーザーが任意の画面階層を持つページにジャンプできちゃうので、対応しないといけない。

なので、スタックで出し入れするのは無理ゲーってことで、画面遷移のスタック全体を再構築できるようにしないといけなくなり、Router APIってのが作られた。

Nagivator.pushなどを使うAPIを Navigator1.0router.goのような Router APIを使うのを Navigatior 2.0と言っていたように思います。

なので、そもそもFlutter Webをやらないプロジェクトなら、Navigator2.0の利用シーンが極めて小さいように思います。ルーティングライブラリを使わんでも生きていけますよ。宣言的になった所で大したメリット無いと思う。

DeepLinkやるならGoRouterしかない

AutoRouteの最新版7.8.x系にはDeepLinkに関して致命的なバグがあります。アプリがフォアグラウンドもしくはバックグラウンドで待機している時にリンクをタップしても、DeepLinkを処理するイベントハンドラーが作動しないというものです。ゼロからプロジェクト作って、AutoRouteだけインストールした状態でも、動かず。
https://github.com/Milad-Akarie/auto_route_library/issues/1722

CAさんのWinTicketでAutoRouteでDeepLink捌いているっていう資料がありましたが、AutoRouteのバージョンが5系で固めているとあったので、そういうことかもしれません。
https://speakerdeck.com/cyberagentdevelopers/guard-todeep-link-wohuo-yong-sita-nabigesiyonguan-li?slide=8

DeepLinkがマストなプロジェクトなら、GoRouterしか選択肢がなくなります。GoRouterは、フォアグラウンド・バックグラウンド、どっちでも動きます。

GoRouterでDeepLinkをさばく方法

単純です。Route作ってパスを指定するだけです。

routes: [
    TypedGoRoute<HomeRoute>(
      path: '/',
      routes: [
        TypedGoRoute<CatalogRoute>(
          path: 'catalog',
        ),
        TypedGoRoute<CategoryRoute>(
          path: 'category/:categoryId',
          routes: [
            TypedGoRoute<ItemRoute>(
              path: 'item/:itemId',
            ),
          ],
        ),
        TypedGoRoute<CampaignRoute>(
          path: 'campaign',
        ),
      ],
    ),
  ],

/campaignに来れば、CampaignRouteが自動的にマッチして呼び出されます。
/category/men/item/222の場合は、:categoryIdmen:itemId222が入ります。

この時、HomeRoute -> CategoryRoute -> ItemRouteの画面階層が再現されます。表示されるのはItemRouteのページだけど、戻るでCategoryRoute、そこから戻ってHomeRouteに移動できます。

context.go("/category/men/item/222")って書くと、上記のような動きをします。goメソッドはスタックそのものを入れ替えます。pushとの違いはそこです。

GoRouterでRouteをネストする

routesをネストすることで親子関係が作れますが、子は親のRouteで定義したパラメーターを全部引き継がないといけません。 ItemRouteは、categoryIdを受け取るようにしないとダメ。

この仕様がマッチしないのは、アプリ内で遷移する時に、ページとなるWidgetにオブジェクトを渡している場合です。文字列や整数じゃなくuserオブジェクト、みたいなケース。

GoRouterは$extraでオブジェクトを渡せますが、親の$extraを子のRouteが受け取れないようです。つまり、親子関係を定義した場合$extraは実質使えなくなります。URLにオブジェクト突っ込めねえし。

個人的にはGoRouterで定義したroutesは、DeepLink Handlerとして受け取るものを書いて、$extraを使う場合はその配下にサブルートを持たないようにしています。

多分ですけど、$extraは苦肉の策で使うもので、GoRouterにおいては非推奨なんじゃないかなって思う。

回遊性のあるナビゲーション対応は無理

ECでよくある、レコメンドの対応ですね。Amazonで商品を見たらおすすめがでます。あれをタップすると、WebであればURLが代わってページが変わり、戻るボタンで前のページに戻ります。

アプリでやると、商品→レコメンドの商品→またレコメンドの商品って感じでPUSHして、スタックを積みます。これをGoRouterでどうやるのか悩んでいます。URLのパスが再帰構造になってしまう(スタックの最終地点が事前にわからない)ので、事前にルートを定義するGoRouterと相性が悪すぎます。

Route Guard ほしい時がある、が。

この画面に遷移するときはこういう条件を満たさないとダメっていうのが、AutoRouteはできる。
GoRouterでもやれなくはない。以下のようにredirectハンドラーを定義すれば、現在のパスが変わったらイベントが走る。でも、ifの嵐になりそう。

import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import 'app_routes.dart';

part 'go_router.g.dart';

(keepAlive: true)
GoRouter router(RouterRef ref) => GoRouter(
      routes: $appRoutes,
      initialLocation: '/splash',
      onException: (context, state, router) {
        return;
      },
      redirect: (context, state) async {
        return null;
      },
    );

GoRouterは開発が盛ん(という名のバグつぶしが多い)印象で、APIもまぁまぁ変わる。バグもまぁまぁ多い、これは仕方ないのかなと思う。ルーティング周りってFlutterの沼っぽいし、Navigator2.0自体が相当面倒なことを頑張ってやっているようなので。

URLはホストの持っているデータ構造を表現するためにコンテキストパス等の考えができたのがそもそもの発想なので、データの階層構造≠画面の階層構造な所が、なかなかどうして。

というわけで、Flutterのルーティングについてのメモでした。おしまい。

Discussion