こんにちは。 モバイルクライアントグループの若宮(id:D_R_1009)です。 最近、スプラトゥーン3のバイトにハマっています。 全ステージでんせつ200を達成できたので、400を目指して日々クマサン商会に入り浸っております。
さて、スタディプラスではFlutter Webを採用しています。
FlutterによるMobile向けとWeb向けの開発では、画面遷移の考え方を変える必要があります。
今回は、開発チームで採用しているgo_router
の使い方を紹介しつつ、Flutter Webにおける宣言的Navigationを紹介します。
go_router
go_router
は、元GoogleのChris Sells氏が開発した、Navigator 2をより簡単に利用するためのライブラリです。
*1
以前は氏の個人リポジトリで開発されていましたが、現在ではFlutterの公式パッケージとして開発されています。
移行対応のIssueはこちら。
Navigator 2はAPIこそシンプルです。 しかし、実アプリケーションの動作に耐えうる実装をするためには、考慮することが非常に多い仕組みです。 次のMediumは、Navigator 2が登場した際に公開された、Navigator 2 APIの解説です。 本ブログは、このMediumを読まずとも、Navigator 2についてなんとなく理解できる事を目指しています。
日本語で解説された記事としては、ntaooさんの下記シリーズがおすすめです。
Navigator 2を利用するには、幾つものハードルを超える必要があります。
ただ、Navigator 2を利用するための学習が非常に大変なことと、実装が難しいため、いくつかのライブラリが公開されています。go_router
は、そのライブラリの1つです。
もしかすると、アプリケーションによっては、go_router
以外のライブラリの方が用途にマッチするかもしれません。
以下にgo_router
以外の代表的なライブラリを挙げておきます。
宣言的Navigation
go_router
を利用していると、既存のAndroidやiOSにおける画面遷移と、多くの点で異なることに戸惑います。
特に、宣言的Navigationが何なのか、そしてなぜ必要なのかの把握が大変です。
そこで、まず、宣言的Navigationの理解から始めます。
最初に、既存のモバイルアプリケーションにおけるNavigationについておさらいします。 Androidの資料が読みやすいので、ここではAndroidを例に出します。 *2
伝統的なAndroidアプリケーションの場合、画面遷移は新たなActivityを、既存のActivityの上に重ねることで実現されます。
簡略化すれば、すべてのAndroidアプリケーションはActivityを重ねたモノです。 複数のアプリケーションを起動した状態は、言い換えれば、複数のActivityを束ねた「アプリケーション」を重ねている状態となります。 重なった「アプリケーション」の一番上にあるものが、ユーザーが操作している画面です。 Androidにおける複数のアプリケーションを管理することは、つまり「アプリケーション」の重なる順番を操作することだと言えます。
この時、「戻る」操作は、重ねたActivityを1つ取り除く処理となります。 画面遷移の経路が重ねられたActivityとして表現され、その状態を管理することで、画面遷移が実装されるということです。 なお、現実のアプリケーションでは、ActivityのLaunchModeやFragment Managerの操作により、この重なり(Stack)が高度に管理されています。
FlutterのNavigator 1*3は、このStackの処理を利用します。
Navigator.push
で1つ階層を重ね、Navigator.pop
で1つ階層を戻すイメージです。
公式ドキュメントには、下記のように記載されています。
Using the Navigator API Mobile apps typically reveal their contents via full-screen elements called "screens" or "pages". In Flutter these elements are called routes and they're managed by a Navigator widget. The navigator manages a stack of Route objects and provides two ways for managing the stack, the declarative API Navigator.pages or imperative API Navigator.push and Navigator.pop.
imperative API
が、AndroidなどのStackと同じ方法を示しています。
declarative API
、つまり宣言的なAPIが、俗にNavigator 2と呼ばれているAPIになります。
Navigationが宣言的、という話を聞いても、まずピンときません。 しかし「あるページが開かれている時、そのページの(Stack上の)下に積まれているページが確定している」という概念であればどうでしょうか?
go_router
を利用すると、次のようにpath
と開くべきページを定義できます。
// GoRouter configuration final _router = GoRouter( routes: [ GoRoute( path: '/', builder: (context, state) => HomeScreen(), ), ], );
この実装は、HomeScreen
が一箇所でのみ開くページとして定義されているならば、「HomeScreen
が開かれているならば、pathは/
」であることを意味します。
また、「アプリ内でpath=/
に画面遷移すれば、HomeScreen
が開かれる」とも言えます。
Stackについては、「ある画面が開かれた時、Stackの状態が決まっている」とも言えます。
例えば、HomeScreen
はStackの一番下であり、ページを開いたときに戻る先はありません。
もしもAndroidのback keyを押したならば、アプリケーションが閉じます。
context.push
と context.go
go_router
には、大きく分けて2つの画面遷移方法があります。
- context.push
Push a location onto the page stack.
- context.go
Navigate to a location.
context.push
は「Navigator 1を利用した」画面遷移であり、context.go
は「Navigator 2を利用した」画面遷移です。
この動作の違いを、簡単に紹介します。
次のようなroutingを考えます。
final _router = GoRouter( routes: [ GoRoute( path: '/', builder: (context, state) => HomeScreen(), routes: [ GoRoute( path: 'detail', builder: (context, state) => DetailScreen(), ), GoRoute( path: 'help', builder: (context, state) => HelpScreen(), ), ], ), ], );
今HomeScreen
を表示しており、そのあとDetailScreen
とHelpScreen
を順々に開くケースを考えます。
この時、次の2つの処理を見てみましょう。
- A:
context.push('/detail')
⇨context.push('/help')
- B:
context.go('/detail')
⇨context.go('/help')
A: 手続き的Navigation
Aの順番に呼び出すと、context.pop
を2回呼び出すことができ、次のような画面遷移します。
HelpScreen
⇨ DetailScreen
⇨ HomeScreen
この画面遷移は、AndroidなどのStackを積み上げて画面遷移するものと同じです。
というのも、context.push
はStackを積み上げて画面遷移します。
複数回同じ画面をpushした場合、呼び出した数Stackに画面が積み重ねられます。
この動作は後述のcontext.go
の思想と異なる振る舞いのため、一度削除が検討されました。
しかし、既存のアプリケーションの動作に合致するため、復活した経緯があります。
議論や実現方法に興味がある方は、次のPRを確認してください。
B: 宣言的Navigation
Bの順番に呼び出すと、context.pop
を1回呼び出すことができ、次のような画面遷移をします。
HelpScreen
⇨ HomeScreen
この画面遷移は、先述の動きと異なっています。
というのもの、context.go
を利用すると、画面遷移が宣言的になされたとみなされるためです。
GoRoute
インスタンスの親子関係を見ると、path=/
の下にpath=/help
のインスタンスが存在しています。
このため、context.go('/help')
を行った場合には、直前に開いていた画面によらず、path=/help
で定義されたStackとなります。
仕組み上、同じpathに対してcontext.go
をしても、呼び出し回数分のStackが積み上がりません。
URLによる遷移
一見、宣言的Navigationは不要そうに見えます。 実際のところ、AndroidやiOSアプリケーションを開発するのであれば、Navigator 2を利用せずとも実装は可能です。
twitter.comRouter(Navigator 2.0, 宣言的ナビゲーション)は、画面遷移スタックを良い感じに更新できる新しい手法であって、既存のナビゲーション上位互換ではない旨が明記された( ´・‿・`)分かる( ´・‿・`)
— mono (@_mono) 2022年1月8日
Router is not an upgrade, just a different widget https://t.co/n0IEEwlkek pic.twitter.com/SmO09NeNyM
宣言的Navigationは、主に、Flutter Webで必要となる仕組みです。
Flutter Webに対応すると「ある画面を開くための方法が、無数に存在する」こととなります。 というのも、URLによる画面遷移に対応する必要があるためです。 これはアドレスバーに直打ちされるケースや、ブックマークをつけて開くケース、テキストリンクにより遷移してくるケースなどさまざまです。
手続き的なNavigationの場合、ある画面の戻る先のページは、直前に開いていたページです。 しかし、この定義では、上記のようなURLにより画面遷移をするケースに対応できません。 また、ブラウザのリロードボタンによる、再読み込みも同様の問題が起きることとなります。
go_router
を利用すると、繰り返しになりますが、「ある画面の戻り先のページ」をGoRoute
インスタンスの親子関係で表現します。
結果として、URLで「ページ」を開いたときに、戻ることになる「ページ」が簡単に定まります。
先ほどの例を挙げると、go_router
では、次のようにpathを定義できます。
final _router = GoRouter( routes: [ GoRoute( path: '/', builder: (context, state) => HomeScreen(), ), GoRoute( path: '/detail', builder: (context, state) => DetailScreen(), ), GoRoute( path: '/help', builder: (context, state) => HelpScreen(), ), ], );
この定義でも、/
と/detail
、そして/help
の3つのアドレスに対応できます。
また、context.push
により、画面遷移と戻り先ページの指定は可能です。
問題は、/help
をブックマークしていたり、途中でブラウザのリロードボタンを押下したケースです。
この定義では、それぞれのScreenはすべてrootのページとなります。
結果として、AppBar
には「戻る」ボタンが表示されません。
この性質を利用すると、「戻る」ボタンが表示されない、複数のrootを持つアプリケーションを作成できます。 MobileとWebでは、これらのNavigationの違いが違和感の有無に直結するため、ぜひ慎重に検討してみてください。
おわりに
Mobileでgo_router
を使うかどうか
先述の通り、Mobile向けの開発では、Navigator 1の採用でほとんどのケースに対応できます。
筆者の感想としては、go_router
を使いこなせるだけの知識がついていれば、MobileでNavigator 2を利用して良さそうです。
宣言的Navigationの良い点は、「あるページに遷移する」方法が余計なハックをせずに2通り提供される点です。 例えば、複数の画面に渡って記録を作り、サーバー送信後に処理結果を表示するケースを考えます。
手続き的なNavigationの場合、一度Stackをクリアした上で、処理結果の画面を開く必要があります。 もしかすると、処理結果の画面から戻る先のページを考慮して、Stackに差し込む実装が必要となるでしょう。 *4
go_router
を利用すると、context.go
が利用できます。
context.go
で開いた処理結果のページは、GoRoute
の親子関係により戻る先のページが規定された状態です。
この構成は、それ以前の手続的な処理に比べて、ミスを起こしにくくなります。
go_router
の学習/追従コスト
go_router
は、Flutterチームが開発するパッケージの中でも、特に変更が激しいパッケージの1つです。
ただ、5系がリリースされたことで、その改善も落ち着きつつあるように見えます。
最近では、go_router
を利用するための手引きも増えつつあり、十分に学習しやすい状況になってきました。
このブログも、助けになれば幸いです。
まとめ
go_router
は高機能なパッケージです。
このため、今回紹介していない機能がたくさんあります。
パッケージのREADMEにたくさん紹介されているので、ぜひ、導入前に見てみてください。
特におすすめなのは、次の項目です。
楽しくFlutterを書いていきましょう!