Flutter の Navigator イケてないなと思って探していたら routemaster という良さげなライブラリ見つけたのでメモも兼ねて紹介します。

routemaster の特徴

routemaster は Flutter の Navigator 2.0 をラッピングしたライブラリで以下の特徴があります。

  • URLとページのマッピングをシンプルに定義できる
  • 使いやすくシンプルな API(例:routemaster.push('/page')
  • ネストされたタブにも、非常に簡単なやり方で対応できる
  • 複数のルートマッピングを定義できる(ログイン済みとログアウト済みのユーザを分けたり)
  • Observer を設定し、ルートの変化を簡単に聞き取ることができる
  • 160個以上の Unit/Widget/Integrationテストで動作検証済み

インストール

flutter pub add routemaster

セットアップ

MaterialApp の routerDelegate に設定する

  • contextありの設定
MaterialApp.router(
  routerDelegate: RoutemasterDelegate(
    routesBuilder: (context) => RouteMap(routes: {
      '/': (routeData) => MaterialPage(child: PageOne()),
      '/two': (routeData) => MaterialPage(child: PageTwo()),
    }),
  ),
  routeInformationParser: RoutemasterParser(),
)
  • contextなしの設定
final routeMap = RouteMap(
 routes: {
   '/': (route) => MaterialPage(child: PageOne()),
   '/two': (route) => MaterialPage(child: PageTwo()),
 },
);

final routemaster = RoutemasterDelegate(
  routesBuilder: (context) => routeMap,
);

MaterialApp.router(
  routerDelegate: routemaster,
  routeInformationParser: RoutemasterParser(),
)

遷移させる

あとは簡単に遷移させるだけ

/// 特定のパスに遷移する
Routemaster.of(context).push('/two')

/// 前のパスに戻る
Routemaster.of(context).pop()

Route にパラメータを持たせる

Path Parameter の場合

// Routemaster.of(context).push('products/123')で 123 がパラメータとして渡る
RouteMap(routes: {
  '/products/:id': (route) => MaterialPage(
        child: ProductPage(id: route.pathParameters['id']),
      ),
  '/products/myPage': (route) => MaterialPage(child: MyPage()),
})

Query Parameter の場合

// Routemaster.of(context).push('/search?query=hello')でhelloがパラメータとして渡る
RouteMap(routes: {
  '/search': (route) => MaterialPage(
        child: SearchPage(query: route.queryParameters['query']),
      ),
})

現在のパス情報の取得

// フルパスの'/product/123?query=param'が取得できる
RouteData.of(context).path;
// Path ParameterのKeyとValueがMapで取得できる → Map: {'id': '123'}
RouteData.of(context).pathParameters;
// Query ParameterのKeyとValueがMapで取得できる → Map: {'query': 'param'}
RouteData.of(context).queryParameters;

Navigation の監視

RoutemasterObserver を継承したクラスを作り、RoutemasterDelegate に設定するだけ

class MyObserver extends RoutemasterObserver {
  @override
  void didPop(Route route, Route? previousRoute) {
    print('ルートが戻ったよ');
  }
  @override
  void didChangeRoute(RouteData routeData, Page page) {
    print('新しいルートだよ: ${routeData.path}');
  }
}

MaterialApp.router(
  routerDelegate: RoutemasterDelegate(
    observers: [MyObserver()],
    routesBuilder: (_) => routeMap,
  ),
  routeInformationParser: RoutemasterParser(),
);

Route ガード

条件によって Route の出し分けが可能です。

  • 条件に一致しない場合、デフォルトの not found ページを表示する。
'/protected-route': (route) =>
    canUserAccessPage()
      ? MaterialPage(child: ProtectedPage())
      : NotFound()
  • 条件に一致しない場合別のURLにリダイレクトさせる
'/protected-route': (route) =>
    canUserAccessPage()
      ? MaterialPage(child: ProtectedPage())
      : Redirect('/no-access'),
  • 条件に一致しない場合別のURLにリダイレクトさせる(URLは変えない)
'/protected-route': (route) =>
    canUserAccessPage()
      ? MaterialPage(child: ProtectedPage())
      : MaterialPage(child: CustomNoAccessPage())

404 ページ

定義されていない URL の場合にエラーページに遷移させるようにする

RouteMap(
    onUnknownRoute: (route, context) {
        return MaterialPage(child: NotFoundPage());
    },
    routes: {
        '/': (_) => MaterialPage(child: HomePage()),
    },
)

リダイレクト

404 以外にもリダイレクトができます。

  • あるルートを別のルートにリダイレクトさせる
RouteMap(routes: {
    '/one': (routeData) => MaterialPage(child: PageOne()),
    '/two': (routeData) => Redirect('/one'),
})
  • 定義されていない Route は全て Top に遷移させる
RouteMap(
  onUnknownRoute: (_) => Redirect('/'),
  routes: {
    '/': (_) => MaterialPage(child: LoginPage()),
  },
)
  • リダイレクト先にパラメータを引き渡す
RouteMap(routes: {
    '/user/:id': (routeData) => MaterialPage(child: UserPage(id: id)),
    '/profile/:uid': (routeData) => Redirect('/user/:uid'),
})

RouteMap の再構築

アプリ起動後の処理によって RouteMap 自体を置き換えることができます。 ログイン前とログイン後で Route が全く異なる場合など

final loggedOutMap = RouteMap(
  onUnknownRoute: (route, context) => Redirect('/'),
  routes: {
    '/': (_) => MaterialPage(child: LoginPage()),
  },
);

final loggedInMap = RouteMap(
  routes: {
    '/': (_) => MaterialPage(child: HomePage()),
  },
);

MaterialApp.router(
  routerDelegate: RoutemasterDelegate(
    routesBuilder: (context) {
      // AppStateの状態によってRouteMapを切り替える
      final appState = Provider.of<AppState>(context);
      return appState.isLoggedIn ? loggedInMap : loggedOutMap;
    },
  ),
  routeInformationParser: RoutemasterParser(),
);

最後に

README を記載しただけになってしまいましたが、いかがでしょうか。 必要な機能はすべて揃っているかなと個人的に思います。 全て実装して試したわけではないので、また必要があれば更新します。 ディープリンクに関しても、対応しているようなので別の記事でまとめてみようと思います。

参考