こんにちは。 モバイルクライアントグループの若宮(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を書いていきましょう!