Top level ShellRoute というパターンの紹介
Flutterでgo_routerを使う中で「Top Level ShellRoute」というパターンが意外とイケると気づいたため紹介します。
Top Level Shell Routeとは
go_routerのルート定義のトップレベルにShellRouteを配置する以下のような実装パターンです。 (勝手に名付けました)
上記のShellRouteはchildを返すだけで追加要素の表示を行わないため一見無駄なShellRouteに見えますが、
以下のように多くのメリットがあることがわかりました
- go_router_builderのルート定義の保守性が上がる
- riverpodのProviderのスコープを作成できる
- main.dartから初期化処理を分離できる
- 自前の通知Widgetなど、オーバーレイ要素の定義をまとめられる
go_router_builderのルート定義の保守性が上がる
これが最大のメリットです。
先ほどのルート定義をgo_router_builderで書き直スト次のようになります。
メンテを楽にするための自動生成なのに、自動生成する前と比べて以下の問題があります
- コード量が増えた
- ルートの構造を把握しづらい
- TypedGoRouteとGoRouteDataの定義を同じファイルに書く必要がある
ここでTop Level ShellRouteパターンを用いると問題を解消することができます。
余談: pathを / とした TypedGoRouteをトップレベルに置くパターンは?
先述の問題を回避するために、/
をpathとしたTypedGoRoute
をトップレベルに置く方法が考えられます。
一見良さそうですが構築されるページのスタックに問題があります。
/login
, /todo
, /done
はスタック一番下の要素にしたいですが、/
がスタックの一番下の要素になってしまいます。
(構築されるスタックのイメージ) 2. login (LoginScreen) 1. / (RootScreen)
Androidの戻るボタンで/login
から/
へ戻れるなど意図しない挙動を引き起こすため、ShellRouteを使用する方が適していると考えています
(アプリの要件によるが)
riverpodのProviderのkeepAliveより若干短い生存期間のproviderを作成できる
Top Level ShellRoteのbuild内でref.watchすることにより、Providerの生存期間をアプリ全体に広げることができます。 現状keepAliveで事足りるため、役に立つかもしれないテクニック程度に考えています。
これはriverpod公式で紹介されているEager initialization of providersというパターンに近いです。 (追加要素の表示をしたい場合、watchするウィジェットとUI構築するウィジェットを分離する必要がある)
main.dartから初期化処理を分離できる
初期化処理をかける場所がmain()
とShellRoute$build
になるため、初期化処理を分離できるようになります。
MaterialApp.router$builder
もあるので具体的なユースケースはあまり思いついていないです。
(強いて言えば初期化処理でcontext経由でgo_routerへアクセスしたいなど?)
自前の通知Widgetなど、オーバーレイ要素の定義をまとめられる
元々ShellRouteは追加要素の表示に使えるため、ShellRoute$build
で画面全体を覆うインジケータやカスタム通知のようなウィジェットを定義することもできます。
1点注意が必要で、Navigator.pushなどによる画面遷移はGoRouterよりも全面に表示されるため、Top Level ShellRouteに定義したオーバーレイ要素が隠れてしまうことがあります。
多くの場合、GoRouterとNavigatorを組み合わせた画面遷移を行うと思うため、使用する場合はこのことに留意する必要があります。
まとめ
Top Level ShellRouteというパターンを紹介しました go_router_builder採用時のメリットは絶大ですが、それ以外のメリットは代替方法あるためプロジェクトに合った方法を採用していくといいなと思いました。
初期化処理をフックできる箇所が増えて将来の実装の選択肢が増やせるため、とりあえずやっておいて損はないと考えてます。