Flutter ~pushNamedの書き方~

@ashdik さんの記事 [【Flutter】もうnamedRouteは使わない！僕が全力で勧めたいルーティング方法をサンプル付きで解説してみた](https://blog.dalt.me/2616)を読んで、いろいろなルーティング方法があるな〜と思い急遽ネタを変えて書いています。

Navigator2.0 については他の方の記事を参考にしてください。

# Navigator の基本的な書き方

よくある書き方は下記のような感じになると思います。

```dart
// push を使った場合
Navigator.push(context, MaterialPageRoute(builder: (_) => MainPage()));

// pushNamed を使った場合
Navigator.pushNamed(context, '/main/');
```
上記の書き方で個人的に感じる問題点としては、

```dart
Navigator.push(context, MaterialPageRoute(builder: (_) => MainPage()));
```
の場合記述量も多く、同じようなコードが増えることになってしまいます。
記述量は増えますが小規模なアプリではこの書き方で十分だと思います。

```dart
Navigator.pushNamed(context, '/main/');
```
の場合はrouteNameの書き間違いや補完も使えないのでイマイチです。　

#上記の問題点の対策
push に関しては [【Flutter】もうnamedRouteは使わない！僕が全力で勧めたいルーティング方法をサンプル付きで解説してみた](https://blog.dalt.me/2616)を参考にしてください。
pushNamed は下記の書き方だとコードジャンプも補完も使えるので簡単にpushNamed で書けるかと思います。

```dart
// 遷移先のメンバ変数のpathでrouteName を指定する
Navigator.pushNamed(context, MainPage.path);

class MainPage extends StatelessWidget {
  // pushNamed で使えるように定義しておく
  static const path = '/main/';
  
  const MainPage({Key key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

```

#弊アプリでの書き方
弊社のサービスではウェブとアプリで同じAPIを使用しているので、遷移先が`/genre/mem/`や`/genre/women/`、`/search/?keyword=財布&page=2`のような感じで渡って来ます。
またレコメンドをしていたり、管理画面からトップに表示するものをコロコロ変えることができるので、1つのボタンの遷移先が複数ある状態です。
ですので、遷移先を決め打ちはできなくて、push を使ったとしても遷移先は正規表現で決めたりする必要があります。

実際に動いているコードは下記のようになっています。

```dart
// バナーやリンクからの遷移
Navigator.pushNamed(context, item.url);

// タグなどテキストから遷移先を決める場合
Navigator.pushNamed(context, GenrePage.path + '?brand=${item.brand}');
```

MaterialAppのonGenerateRouteでは下記のような処理をしています。

```dart
// 記述量の問題で変更していますが、実際はクラス名.path で書いています。
final router = Rotuer.create({
  '/home/': (context, args) => const HomePage(),
  '/genre/:gender/': (context, args) => GenrePage.fromArguments(args.args),
  '/search/': (context, args) => SearchPage.fromArguments(args.args),
});

abstract class Rotuer {
  Route<dynamic> generateRoute(RouteSettings settings);

  factory Router.create(Map<String, RouteBuilder> routeMap) => _RouterImpl(routeMap);
}

class _RouterImpl implements Router {
  final List<RouteEntity> _routerDict;

  _RouterImpl(Map<String, RouteBuilder> routeMap)
      : _routerDict = <RouteEntity>[
          for (var key in routeMap.keys)
            _buildRouteEntry(
              key,
              routeMap[key],
            ),
        ];

  // MaterialAppのonGenerateRouteに渡す関数
  Route generateRoute(RouteSettings settings) {
    try {
      final path = settings.name;
      Map query = <String, dynamic>{};

      // クエリストリングを取り除く
      if (path.contains('?')) {
        query = Uri.parse(path).queryParametersAll;
        path = path.split('?').first;
      }

      // 正規表現でマッチするパスがあるか確認する
      final routePath = _getRoutePath(path);
      RegExpMatch match;
      final routeEntry = _routerDict.firstWhere((r) {
        match = r.regex.firstMatch(routePath);
        return match != null;
      }, orElse: () => null);

      if (routeEntry == null) {
        // 対応していないパスはwebViewで開く
        throw RouterNotFoundException();
      }

      // 正規表現の名前付きキャプチャのリスト
      var names = <String>[];
      if (match.groupCount > 0 && match.groupNames.isNotEmpty) {
        names = match.groupNames.toList();
      }
      
      // /genre/men/ で遷移した場合は、{'gender': 'men'} を作る
      // 
      // /genre/:gender/:type を用意していて /genre/men/tops/に遷移した場合は
      // {'gender': 'men', 'type': 'tops'}を作ることもできる
      final args = <String, String>{
        for (var name in names) name: match.namedGroup(name),
      };

      return CupertinoPageRoute(
        settings: settings,
        builder: (context) => routeEntry.routeBuilder(
          context,
          RouteArgs(
            args..addAll(query),
            settings.arguments,
          ),
        ),
      );
    } on RouterNotFoundException {
      String url = setting.name;
      if (!url.startsWith('https')) {
        url = BASE_URL + url;
      }
      return CupertinoPageRoute(
        builder: (context) => WebViewWidget(url: url),
      );
    }
  }
}

RouteEntity _buildRouteEntry(String name, RouteBuilder routeBuilder) {
  final params = _getRoutePath(name).replaceAllMapped(
    RegExp(':([a-zA-Z0-9_-]+)'),
    (match) {
      final groupName = match.group(1);
      return '(?<$groupName>.+[^/]+)';
    },
  );
  final regEx = RegExp('^$params\$', caseSensitive: false);
  return RouteEntity(name, regEx, routeBuilder);
}

String _getRoutePath(String name) {
  final parts = name.trim().split('/');
  parts.removeWhere((val) => val == '');
  parts.map((val) {
    if (val.startsWith(':')) {
      return val;
    } else {
      return val.toLowerCase();
    }
  });
  return parts.join('/');
}

typedef RouteBuilder = Widget Function(BuildContext context, RouteArgs args);

class RouteArgs {
  /// path で指定したURLの :id/ の部分
  /// query_string
  final Map<String, dynamic> args;

  /// settings で渡した引数
  final Object body;

  RouteArgs(
    this.args, [
    this.body,
  ]);

  String operator [](String key) => args[key];
}

class RouteEntity {
  final String name;
  final RegExp regex;
  final RouteBuilder routeBuilder;

  RouteEntity(
    this.name,
    this.regex,
    this.routeBuilder,
  );
}
```

#まとめ
ページ数の少ないアプリやページ構成変わらないようなアプリの場合、pushで書くほうが手間も少なく良いと思います。
ページ数が増えたり、onGenerateRoute の再定義は必要ですが、WEBと似た動きやWEBと同じAPIを使用する場合にはこのような書き方がハマると思います。
~~この書き方で一番めんどくさいのはonGenerateRouteを再定義した後に`hot restart`が必要でFlutterの`hot reload`の恩恵を受けれないことです...~~

実際に動くコードを Github で公開しています。
気になる方は下記のリンクから見てみてください!
[サンプルコード](https://github.com/0maru/regext_router_sample)

Twitter もやっています。
よければフォローしてください
https://twitter.com/0maru_dev