パスを使った場合のNavigator2の利用方法
続いて、パスによる画面遷移もサポートしたNavigator2での使い方を説明します。先ほど同じ画面遷移を行うサンプルを用意し、その動作イメージと主要要素の関係を示したのが図3です。
前回に比べて大きく複雑になりました。全体を見ても今ひとつイメージがつかめないと思います。赤い部分として示した部分が前回と大きく異なる部分です。これらの違いについて、それぞれ説明します。
新しい要素の関係について
今回、新しく追加しているクラスの関係についてのみに着目して、その役割を示したものが図4です。
Routerが全体を管理します。RouterDelegateクラスは画面スタックとパスとの同期を管理しています。そして、RouterInformationParserでパス表現との同期を管理します。
これらを使ったコード例がリスト3です。
class _MainApp2 extends State{ ItemRouterDelegate _routerDelegate = ItemRouterDelegate(); ItemRouterInformationParser _routerInformationParser = ItemRouterInformationParser(); @override Widget build(BuildContext context) { // (1) MaterialApp.routerを使ったインスタンスの作成 return MaterialApp.router( title: "Nav2 App", debugShowCheckedModeBanner : false, // (2) パスとの同期を管理 routeInformationParser: _routerInformationParser, // (3) 画面スタックを管理 routerDelegate: _routerDelegate, ); } }
先ほどのサンプルでは、MaterialAppを使っていましたが、今回は(1)Routerを作るために、MaterialApp.router()コンストラクタを使って作成します。そして、(2)routeInformationParserプロパティと(3)routerDelegateプロパティにそれぞれのインスタンスを設定します。
また、今回はパスを管理するクラスが必要になります。このクラスはプログラム上でのパスの種類が管理できればどのような実装でも構いません。今回は、リスト4のように実装しました。
class AppRoutePath {
final String id;
AppRoutePath.list() : id = null;
AppRoutePath.item(this.id);
bool get isListPage => id == null;
bool get isItemPage => id != null;
}
RouterDelegateクラス
パスを使わないサンプルでは、NavigatorクラスをWidgetとして直接利用しました。しかし、パスを使う場合には直接Navigatorクラスを使わず、RouterDelegateクラスを継承したクラスを自作します。そのクラスが外部と同期をしつつNavigator内のpagesプロパティを操作します。リスト5はRouterDelegateクラスの実装例です。
// (1) RouterDelegateの実装クラスを作成する class ItemRouterDelegate extends RouterDelegatewith ChangeNotifier, PopNavigatorRouterDelegateMixin { final GlobalKey navigatorKey; ProductItem _selectedItem; List items = [ ProductItem("id1", "商品A"), ProductItem("id2", "商品B") ]; ItemRouterDelegate() : navigatorKey = GlobalKey (); // (2) 表示するアイテムが選択された時の処理 void _onTapItem(ProductItem item) { _selectedItem = item; // (3) 変更通知をする notifyListeners(); } @override Widget build(BuildContext context) { return Navigator( key: navigatorKey, pages: [ MaterialPage( key: ValueKey('ProductListPage'), child: ProductListPage(items, _onTapItem)), if (_selectedItem != null) MaterialPage(child: ProductItemPage(_selectedItem)) ], onPopPage: (route, result) { print('on pop page'); if (!route.didPop(result)) { return false; } _selectedItem = null; // (4) 変更通知をする notifyListeners(); return true; }, ); } // (5) 新しいパスが設定された時 @override Future setNewRoutePath(AppRoutePath path) async { if (path.isItemPage) { // パスに従った詳細ページ用のデータを設定する _selectedItem = items.firstWhere((element) => element.id == path.id); } return; } // (6) 現在の状態をパスで表現した場合 AppRoutePath get currentConfiguration => _selectedItem == null ? AppRoutePath.list() : AppRoutePath.item(_selectedItem.id); }
ItemRouterDelegateクラスは、StatefulWidgetクラスの実装ではありません。そのため、(2)のように、変更を通知するためにChangeNotifierクラスと、pop処理を行うためにPopNavigatorRouterDelegateMixinクラスをミックスインとして利用します。
そして、リストのアイテム部分をタップされた時の処理を(2)のように定義します。今回はsetStateの代わりに(3)のようにnotifyListenersメソッドを使って変更されたことを通知します。
この変更は、画面更新だけではなく、RouteInformationParserにも通知されます。同様に(4)でもnotifyListenersメソッドをコールします。ここまでの処理で画面更新は完了です。
そして、以降のコードがパスと同期をするためのコードになります。(5)はパスが変更された時に、画面を同期するための処理です。パス上にProductItemを識別するidを抽出して、該当のインスタンスを設定しています。(6)は現在の状態をパスで表現した場合の処理です。(5)と(6)を通じてAppRoutePathとの同期を行います。
RouterInformationParserクラス
RouterInformationParserクラスは、パス(文字列表現)との同期を行うためのクラスで、リスト6がサンプルの実装コードです。
class ItemRouterInformationParser extends RouteInformationParser{ // (1) パスが変更された時の処理 @override Future parseRouteInformation( RouteInformation routeInformation) async { if (routeInformation.location == '/') { return AppRoutePath.list(); } else { // (2) /id1 のような入力になるので、最初の"/"をとる var id = routeInformation.location.substring(1); return AppRoutePath.item(id); } } // (3) 状態が変わった時に、パスを反映する処理 @override RouteInformation restoreRouteInformation(AppRoutePath path) { if (path.id != null) { // (4) アイテムが設定されている場合のURLを設定する return RouteInformation(location: '/' + path.id); } return RouteInformation(location: '/'); } }
パスが外部(ブラウザの場合であればURLを直接指定など)から変更された場合には、(1)parseRouteInformationメソッドがコールされるので、アプリケーション内のAppRoutePathインスタンスを作成し返します。詳細ページの場合には、(2)のように入力された文字列から判断します。一方、内部の状態が変わった場合には、(3)restoreRouteInformationメソッドがコールされるので、AppRoutePathインスタンスからパスの文字列を作成します。
まとめ
Navigator2を使うか、Navigator1を使うかを迷われた場合に、1つの選択肢として、パス表現を使うかどうかが大きな判断ポイントになると思います。今回のサンプルのようにシンプルな構造であれば、Navigator2を使わなくても問題ないはずです。
一方、複雑なアプリケーションになると、自分が作った画面をすぐに確認するためにパス表現が使えるのは便利です。また、広告関連であると外部から直接アプリ内の特定画面に直接アクセスすることも多々あるので、そのようなケースでは便利になるはずです。
