[Flutter] Navigatorライブラリのroutemasterを紹介する

FlutterのNavigatorイケてないなと思って探していたら[routemaster](https://github.com/tomgilder/routemaster)という良さげなライブラリ見つけたのでメモも兼ねて紹介します。


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

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

# インストール
```shell
flutter pub add routemaster
```

# セットアップ

## MaterialAppのrouterDelegateに設定する

- contextありの設定

```dart
MaterialApp.router(
  routerDelegate: RoutemasterDelegate(
    routesBuilder: (context) => RouteMap(routes: {
      '/': (routeData) => MaterialPage(child: PageOne()),
      '/two': (routeData) => MaterialPage(child: PageTwo()),
    }),
  ),
  routeInformationParser: RoutemasterParser(),
)
```
- contextなしの設定

```dart
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(),
)
```

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

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

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


## Routeにパラメータを持たせる
### Path Parameterの場合

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

### Query Parameterの場合

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

### 現在のパス情報の取得
```dart
// フルパスの'/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に設定するだけ

```dart
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ページを表示する。`

```dart
'/protected-route': (route) => 
    canUserAccessPage()
      ? MaterialPage(child: ProtectedPage())
      : NotFound()
```

- `条件に一致しない場合別のURLにリダイレクトさせる`

```dart
'/protected-route': (route) => 
    canUserAccessPage()
      ? MaterialPage(child: ProtectedPage())
      : Redirect('/no-access'),
```

- `条件に一致しない場合別のURLにリダイレクトさせる（URLは変えない）`

```dart
'/protected-route': (route) => 
    canUserAccessPage()
      ? MaterialPage(child: ProtectedPage())
      : MaterialPage(child: CustomNoAccessPage())
```


## 404ページ

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

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

## リダイレクト

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

- `あるルートを別のルートにリダイレクトさせる`

```dart
RouteMap(routes: {
    '/one': (routeData) => MaterialPage(child: PageOne()),
    '/two': (routeData) => Redirect('/one'),
})
```

- `定義されていないRouteは全てTopに遷移させる`

```dart
RouteMap(
  onUnknownRoute: (_) => Redirect('/'),
  routes: {
    '/': (_) => MaterialPage(child: LoginPage()),
  },
)
```


- `リダイレクト先にパラメータを引き渡す`

```dart
RouteMap(routes: {
    '/user/:id': (routeData) => MaterialPage(child: UserPage(id: id)),
    '/profile/:uid': (routeData) => Redirect('/user/:uid'),
})
```


## RouteMapの再構築

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

```dart
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を記載しただけになってしまいましたが、いかがでしょうか。
必要な機能はすべて揃っているかなと個人的に思います。
全て実装して試したわけではないので、また必要があれば更新します。
[ディープリンク](https://github.com/tomgilder/routemaster/wiki/Routemaster-Flutter-scenarios)に関しても、対応しているようなので別の記事でまとめてみようと思います。

## 参考
- https://github.com/tomgilder/routemaster
- https://note.com/mxiskw/n/n3fc9e68fa528