Flutter for Webで可変パスとブラウザからの直接アクセスへの対応

Flutter for Webではルーティングを設定することで、そのパスへアクセスした時に特定のウィジェットを呼び出せます。

## シンプルな方法

via [flutter-webでURLをつける方法 - Qiita](https://qiita.com/bucchi0426/items/51e123a1ae316429696f)。

```dart
class HomePage extends StatelessWidget {
  //このClassのURLを以下で定義する
  static const String route = '/mypage';
```

そして `MaterialApp` の初期化時に `routes` でパスを指定します。

```dart
MaterialApp(
    initialRoute: HomePage.route,
    routes: {
      HomePage.route: (context) => HomePage(),
      LoginPage.route: (context) => LoginPage(),
```

この方法の場合、 `/users/100` とか `/users/100/products` といった可変パスに対応できません。

## fluroを使う

そこでfluroを使います。例えば以下のようにして `/github/ユーザ名/リポジトリ名` みたいなパスに対応します。詳細は使い方は [【Flutter】ルーティングライブラリFluroの使い方 - 中安拓也のブログ](https://www.l08084.com/entry/2021/12/29/180118)が参考になります。

```dart
import 'package:flutter/material.dart';
import 'package:fluro/fluro.dart';
import '../pages/home_page.dart';
import '../pages/learning.dart';

Handler createBasicHandler(Widget targetWidget) {
  return Handler(
      handlerFunc: (BuildContext? context, Map<String, List<String>> params) {
    return targetWidget;
  });
}

Handler learningPageHandler = Handler(
    handlerFunc: (BuildContext? context, Map<String, List<String>> params) {
  return LearningPage(
      userName: params['userName']!.first,
      repository: params['repository']!.first);
});

class Routes {
  static void configureRoutes(FluroRouter router) {
    router
      ..define('/', handler: createBasicHandler(const HomePage()))
      ..define('/github/:userName/:repository', handler: learningPageHandler);
  }
}
```

後はNavigatorを拡張した `FluroRouter` の `navigateTo` で画面遷移できます。

```dart
MyApp.router!.navigateTo(context, '/github/goofmint/test-repo')
```

アプリの初期化時には routes ではなく `onGenerateRoute` で指定します。

```dart
class MyApp extends StatelessWidget {
  static FluroRouter? router;
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Manabiya',
        debugShowCheckedModeBanner: false,
        theme: ThemeData.light(),
        darkTheme: ThemeData.dark(),
        initialRoute: '/',
        onGenerateRoute: MyApp.router!.generator;
    );
  }
}
```

## Webブラウザから直アクセスするとエラーが出る

ここまでは良いのですが、画面遷移した `/github/goofmint/test-repo` にブラウザから直接アクセスするとエラーが出ました。

```
Expected a value of type 'Handler', but got one of type 'Null'
See also: https://flutter.dev/docs/testing/errors
```

このエラーが出る原因が全然分からなかったのですが、 `onGenerateRoute` を実行した結果、HandlerではなくNullが返ってくるのでエラーになったのだろうと推測しました。そこで、 [dart - Difference between onGenerateRoute and routes in Flutter - Stack Overflow](https://stackoverflow.com/questions/59822279/difference-between-ongenerateroute-and-routes-in-flutter) を参考にonGenerateRouteの内容を見てみました。

```dart
class MyApp extends StatelessWidget {
  static FluroRouter? router;
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Manabiya',
        debugShowCheckedModeBanner: false,
        theme: ThemeData.light(),
        darkTheme: ThemeData.dark(),
        initialRoute: '/',
        onGenerateRoute: (setting) {
          // settingは文字列ではないのでdebugPrintではなくprintにしています
          print(setting);
          return MyApp.router!.generator(setting);
        });
  }
}
```

その結果、こんなログが確認できました。

```
RouteSettings("/", null)
RouteSettings("/github", null)
```

パスを `/` で区切って、順番にアクセスしているようです。 `/github` は元々想定していなかったので、この設定が必要とのことです。

さらに `/github/:userName` も必要なので、次のようにルーティング設定を追加します。不要なルーティングはすべてHomePageを表示するようにしていますが、本当な何らか用意すべきかも知れません。

```dart
router
  ..define('/', handler: createBasicHandler(const HomePage()))
  ..define('/github', handler: createBasicHandler(const HomePage()))
  ..define('/github/:userName',
      handler: createBasicHandler(const HomePage()))
  ..define('/github/:userName/:repository', handler: learningPageHandler);
```

ちなみに `/github/goofmint/test-repo/` に画面遷移するようにした場合、最後の `/` がない場合とある場合、両方のルーティングが必要です。

```dart
router
  ..define('/', handler: createBasicHandler(const HomePage()))
  ..define('/github', handler: createBasicHandler(const HomePage()))
  ..define('/github/:userName',
      handler: createBasicHandler(const HomePage()))
  ..define('/github/:userName/:repository', handler: learningPageHandler)
  ..define('/github/:userName/:repository/', handler: learningPageHandler);
```

面倒な場合は最後の `/` はない画面遷移にするのが良さそうです。

## まとめ

Flutter for WebはSEO対策さえ無視できれば（認証必須のWebアプリの場合など）、かなり使いやすいWebフロントエンドフレームワークではないかと思います。SelextableTextを使うことでテキストのコピペもできます。URLのルーティングもfluroを使えば（ハマりましたが）自由度高くできそうです。

もしURLの `/#/` を消したい場合には[url_strategy](https://pub.dev/packages/url_strategy)を使えば簡単に削除できます。