Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

入門go_router

こんにちは。 モバイルクライアントグループの若宮(id:D_R_1009)です。 最近、スプラトゥーン3のバイトにハマっています。 全ステージでんせつ200を達成できたので、400を目指して日々クマサン商会に入り浸っております。

さて、スタディプラスではFlutter Webを採用しています。 FlutterによるMobile向けとWeb向けの開発では、画面遷移の考え方を変える必要があります。 今回は、開発チームで採用しているgo_routerの使い方を紹介しつつ、Flutter Webにおける宣言的Navigationを紹介します。

pub.dev

go_router

go_router は、元GoogleのChris Sells氏が開発した、Navigator 2をより簡単に利用するためのライブラリです。 *1 以前は氏の個人リポジトリで開発されていましたが、現在ではFlutterの公式パッケージとして開発されています。

github.com

移行対応のIssueはこちら。

github.com


Navigator 2はAPIこそシンプルです。 しかし、実アプリケーションの動作に耐えうる実装をするためには、考慮することが非常に多い仕組みです。 次のMediumは、Navigator 2が登場した際に公開された、Navigator 2 APIの解説です。 本ブログは、このMediumを読まずとも、Navigator 2についてなんとなく理解できる事を目指しています。

medium.com

日本語で解説された記事としては、ntaooさんの下記シリーズがおすすめです。

zenn.dev

zenn.dev

Navigator 2を利用するには、幾つものハードルを超える必要があります。 ただ、Navigator 2を利用するための学習が非常に大変なことと、実装が難しいため、いくつかのライブラリが公開されています。go_routerは、そのライブラリの1つです。

もしかすると、アプリケーションによっては、go_router以外のライブラリの方が用途にマッチするかもしれません。 以下にgo_router以外の代表的なライブラリを挙げておきます。

pub.dev

pub.dev

pub.dev

宣言的Navigation

go_routerを利用していると、既存のAndroidやiOSにおける画面遷移と、多くの点で異なることに戸惑います。 特に、宣言的Navigationが何なのか、そしてなぜ必要なのかの把握が大変です。

そこで、まず、宣言的Navigationの理解から始めます。


最初に、既存のモバイルアプリケーションにおけるNavigationについておさらいします。 Androidの資料が読みやすいので、ここではAndroidを例に出します。 *2

developer.android.com

伝統的なAndroidアプリケーションの場合、画面遷移は新たなActivityを、既存のActivityの上に重ねることで実現されます。

簡略化すれば、すべてのAndroidアプリケーションはActivityを重ねたモノです。 複数のアプリケーションを起動した状態は、言い換えれば、複数のActivityを束ねた「アプリケーション」を重ねている状態となります。 重なった「アプリケーション」の一番上にあるものが、ユーザーが操作している画面です。 Androidにおける複数のアプリケーションを管理することは、つまり「アプリケーション」の重なる順番を操作することだと言えます。

この時、「戻る」操作は、重ねたActivityを1つ取り除く処理となります。 画面遷移の経路が重ねられたActivityとして表現され、その状態を管理することで、画面遷移が実装されるということです。 なお、現実のアプリケーションでは、ActivityのLaunchModeやFragment Managerの操作により、この重なり(Stack)が高度に管理されています。

developer.android.com

developer.android.com

FlutterのNavigator 1*3は、このStackの処理を利用します。 Navigator.pushで1つ階層を重ね、Navigator.popで1つ階層を戻すイメージです。

api.flutter.dev

公式ドキュメントには、下記のように記載されています。

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.pushcontext.go

go_routerには、大きく分けて2つの画面遷移方法があります。

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を表示しており、そのあとDetailScreenHelpScreenを順々に開くケースを考えます。 この時、次の2つの処理を見てみましょう。

  • A: context.push('/detail')context.push('/help')
  • B: context.go('/detail')context.go('/help')
A: 手続き的Navigation

Aの順番に呼び出すと、context.popを2回呼び出すことができ、次のような画面遷移します。

HelpScreenDetailScreenHomeScreen

この画面遷移は、AndroidなどのStackを積み上げて画面遷移するものと同じです。 というのも、context.pushはStackを積み上げて画面遷移します。

複数回同じ画面をpushした場合、呼び出した数Stackに画面が積み重ねられます。 この動作は後述のcontext.goの思想と異なる振る舞いのため、一度削除が検討されました。 しかし、既存のアプリケーションの動作に合致するため、復活した経緯があります。 議論や実現方法に興味がある方は、次のPRを確認してください。

github.com

B: 宣言的Navigation

Bの順番に呼び出すと、context.popを1回呼び出すことができ、次のような画面遷移をします。

HelpScreenHomeScreen

この画面遷移は、先述の動きと異なっています。 というのもの、context.goを利用すると、画面遷移が宣言的になされたとみなされるためです。

GoRouteインスタンスの親子関係を見ると、path=/の下にpath=/helpのインスタンスが存在しています。 このため、context.go('/help')を行った場合には、直前に開いていた画面によらず、path=/helpで定義されたStackとなります。

仕組み上、同じpathに対してcontext.goをしても、呼び出し回数分のStackが積み上がりません。

URLによる遷移

一見、宣言的Navigationは不要そうに見えます。 実際のところ、AndroidやiOSアプリケーションを開発するのであれば、Navigator 2を利用せずとも実装は可能です。

twitter.com

宣言的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系がリリースされたことで、その改善も落ち着きつつあるように見えます。

pub.dev

最近では、go_routerを利用するための手引きも増えつつあり、十分に学習しやすい状況になってきました。 このブログも、助けになれば幸いです。

まとめ

go_routerは高機能なパッケージです。 このため、今回紹介していない機能がたくさんあります。 パッケージのREADMEにたくさん紹介されているので、ぜひ、導入前に見てみてください。

特におすすめなのは、次の項目です。

楽しくFlutterを書いていきましょう!

*1:"Chris Sells is a Google Product Manager on the Flutter development experience"との記述がmediumにあります

*2:また、Androidにはback keyがあるため、より複雑であると思われます。

*3:2登場以前のNavigator

*4:筆者はAndroidのFragment Managerで頑張った記憶があります。