Navigatorをネストしてタブ内遷移を実現する
Navigatorとは
おそらくNavigatorと聞いて真っ先に思い浮かぶのは、画面遷移時に使うNavigator.of(context).push/popではないでしょうか。
今まで僕はおまじない的に画面遷移時にNavigatorを使っていましたが、僕が所属しているチームの案件でNavigatorに関して少し潜る機会があったので、この記事で知見を共有したいと思います。
NavigatorはWidget
Navigatorは実はWidgetです。.of(context)を使うのでなんとなくそうなのかなと思っていた人もいると思います。
ちなみに、NavigatorはStatefulWidgetで、.of(context)ではancestorStateOfTypeを使用して自身のStateであるNavigatorStateにアクセスしてそれをreturnします。Scaffoldのofメソッドと同じです。
NavigatorはWidgetsAppの中で使われています。WidgetsAppはMaterialAppでもCupertinoAppでもbuildメソッドで使用されているクラスで、要はアプリを作る際の基礎のWidgetの内部で使われています。
そのため、普段WidgetとしてのNavigatorを意識することは少なく、僕のようにおまじない的に画面遷移時にNavigatorを使っている方が多いのではないでしょうか。
どんな時にNavigatorを使うのか
ではどんな時に使うのかということですが、WidgetsApp内のNavigatorによるnavigationとは別の内部のnavigationを実現したい時に使います。
例えば、この記事ではタブ内で画面遷移をする時を例にあげます。ここで言う「タブ」とはFlutterで言うBottom Navigationを指します。
タブ内のWidgetから普通にNavigator.of(context)を使うと、タブを表示しているページの上からかぶさるようにして画面遷移をします。
そうでなく、タブを表示したままそのタブ内で画面遷移をするようにしたい時にNavigatorを使います。
この場合WidgetsApp内でNavigatorが使用されているため、1つのアプリに2つのNavigatorが存在することになります。
この状態をこの記事では「Navigatorをネストする」と表しています。
ちなみに、CupertinoTabViewでは内部でNavigatorを使用しています。そのため、Bottom NavigationにCupertinoTabViewを使用した場合はこちらがNavigatorを使用しなくてもタブ内遷移になります。
タブ内遷移する場合のNavigatorの使い分け
普段画面遷移する場合はNavigator.of(context).push/popをすると思いますが、Navigatorをネストした場合は注意が必要です。
先ほども部分的に述べましたが、Navigatorのofメソッドは以下のような実装になっています(Flutter v1.9.1+hotfix.6現在)。
普通に.of(context)と指定した場合はrootNavigatorがfalseなので、最も近い祖先のNavigatorStateを使用します。
つまり、Navigatorをネストする場合は、どのNavigatorを使って遷移するのかを意識しなければならなくなります。
例えばタブ内遷移の場合、ネストしたNavigatorの子孫の中でタブ内遷移したい場合はNavigator.of(context)を使えばいいですが、タブ内遷移でなくタブ外遷移(WidgetsAppのNavigatorを使った遷移)をしたい場合はNavigator.of(context, rootNavigator: true)を使わなければなりません。
rootNavigatorをtrueにする他にも、GlobalKeyを使って直接NavigatorStateにアクセスする方法もあります。
NavigatorのkeyにGlobalKeyを渡してそのGlobalKeyのcurrentStateを取得する方法です。
Navigatorのofメソッドはrootを使うかどうかしか指定できないため、祖先側からネストしたNavigatorを使って画面遷移したい場合や、Navigatorを三重にネストしていて間のNavigatorにアクセスしたい場合などにはこの方法が必要かもしれません。
NavigatorObserverを使用する場合
上記のようにして、僕のチームでもタブ内遷移を実現できましたが、ここでひとつつまづきがありました。
僕のチームではAnalyticsログのために、MaterialAppのnavigatorObserversにnavigationを監視してページ表示のログを送るNavigatorObserverを指定していました(MaterialAppのnavigatorObserversは内部でNavigatorのobserversに渡される)。
ですが、これがネストしたNavigatorを使ったnavigationでは機能していなかったのです。
シンプルにネストしたNavigatorのobserversにもそのNavigatorObserver指定すればいいと思ったのですが、NavigatorObserverはひとつのインスタンスにひとつのNavigatorを紐づけるため、インスタンスを別にする必要があります。
元々MaterialAppのnavigatorObserversにシングルトンチックな状態を持つNavigatorObserverを渡していたため、NavigatorObserverの改修も必要になりました。
同じようにNavigatorObserverを使用する場合は注意が必要そうです。
Navigatorをネストする際のポイント整理
ポイントは以下です。
・タブ内遷移のように、WidgetsApp内のNavigatorによるnavigationとは別の内部のnavigationを実現したい時にNavigatorをネストする。
・ネストしたNavigatorの子孫で画面遷移する場合は、どのNavigatorを使って遷移するかを意識する必要がある。
・NavigatorObserverはNavigatorごとに指定する必要がある。1つのobserverのインスタンスは1つのNavigatorしか監視できない。
公式のNavigatorのドキュメントの「Nesting Navigators」項にも例と共に解説があるので読んでみてください。
簡単なサンプルアプリ的なものもGitHubにあげているので、動きを見たい方はそちらもどうぞ。
Twitterはこちら https://twitter.com/kitoko552