【Flutter】MaterialAppにて最初に起動させたいページの設定方法について


# 結論

MaterialAppを使ってるアプリにおいて、最初に起動させたいページを設定するには、以下の2つの方法がある。

- homeプロパティに記述する

```entry.dart
    return MaterialApp(
      title: "NavigatorApp",
      home: SplashPage(),               // １. 最初に起動させたいページ
      routes: <String, WidgetBuilder>{  // 2. routesには「/」を含めない。
        '/welcome': (BuildContext context) => WelcomePage(),
        '/home': (BuildContext context) => HomePage(),
        '/settings': (BuildContext context) => SettingsPage(),
      },
    );
```

- routesプロパティまたは、onGenerateRoutes内で「/」のエントリを定義する。

```entry.dart
    return MaterialApp(
      title: "NavigatorApp",
      routes: <String, WidgetBuilder>{
        '/': (BuildContext context) => SplashPage(),         // １. 最初に起動させたいページを「/」で定義
        '/welcome': (BuildContext context) => WelcomePage(),
        '/home': (BuildContext context) => HomePage(),
        '/settings': (BuildContext context) => SettingsPage(),
      },
    );
```

# 背景

なんでこの記事を書いたかというとFlutterのRouteの設定周りで少しハマったためです。その時の話を以下に紹介します。

FlutterではMatertialAppというウィジェットがあり、基本アプリのトップレベルで定義することが多いと思います。書き方としては、buildメソッドの中で、以下のような定義をすると思います。

```entry.dart
    return MaterialApp(
      title: "NavigatorApp",
      initialRoute: '/',
      routes: <String, WidgetBuilder>{
        '/': (BuildContext context) => HomePage(),
        '/settings': (BuildContext context) => SettingsPage(),
      },
    );
```

ここで大事な部分は、「initialRoute」というプロパティと、「routes」というプロパティです。

### routes
名前付きルートを使って遷移する場合に使う識別子（文字列）をキーとし、対応するウィジェットを値としてMapで記述する。

### initialRoute
アプリを起動した際に最初に遷移されるRouteの識別子（だと思ってた）

## 何が問題だったか

例えば、ログイン状態の有無によって、アプリの初期起動時に表示されるページを切り替えたい場合があるとします。その場合、Proxyとなるページ(/splash)を最初に起動させ、認証状態を確認し、ログイン済みであれば、「ホーム画面（/）」、ログインしていなければ「Welcome画面（/welcome）」に遷移させるということをすると思います。

これをやろうとした場合最初以下のように定義しました。initialRouteにProxyとなる「/splash」というルートを設定すれば、最初は、/splashのページが起動することができるだろうと。。

```entry.dart
    return MaterialApp(
      title: "NavigatorApp",
      initialRoute: '/splash',
      routes: <String, WidgetBuilder>{
        '/splash': (BuildContext context) => Provider<SplashBloc>(  // Providerを使って、Blocを注入する想定
              builder: (context) => SplashBloc(),
              child: SplashPage(),
              dispose: (_, bloc) {
                bloc.dispose();
              },
            ),
        '/welcome': (BuildContext context) => WelcomePage(),
        '/': (BuildContext context) => HomePage(),
        '/settings': (BuildContext context) => SettingsPage(),
      },
    );

```

### なぜかinitialRouteの「/splash」が無視され、ホーム画面（/）が表示される

そうなんです。なぜか上記で設定した「/splash」のルートが起動されず、無視された挙句「/」のルートが起動されました。

### 原因調査

MaterialAppの実装を見てみると、以下のように書いてありました。

> /// At least one of [home], [routes], [onGenerateRoute], or [builder] must be
  /// non-null. If only [routes] is given, it must include an entry for the
  /// `[Navigator.defaultRouteName](/)`, since that is the route used when the
  /// application is launched with an intent that specifies an otherwise
  /// unsupported route.

> home, routes, onGenerateRoute, builderの中で少なくとも一つはnon-nullでなくてはならない。routeが与えられた時、そのrouteはアプリケーションが起動される時（割愛）に使われるので、`Navigator.defaultRouteName`（`/`）のためのエントリーを含んでいる必要があります。

つまり、homeプロパティが設定されていない場合は、routesの中からエントリが決まり、「/」に対応するエントリ（ページ）を含んでいる必要があるということです。

また、次のようにも書いてありました。

> /// The [MaterialApp] configures the top-level [Navigator] to search for routes
/// in the following order:
///
///  1. For the `/` route, the [home] property, if non-null, is used.
///
///  2. Otherwise, the [routes] table is used, if it has an entry for the route.
///
///  3. Otherwise, [onGenerateRoute] is called, if provided. It should return a
///     non-null value for any _valid_ route not handled by [home] and [routes].
///
///  4. Finally if all else fails [onUnknownRoute] is called.

> MaterialAppはRouteの探索のため、以下の順序にしたがって、トップレベルのNavigatorを設定します。
1. homeプロパティがnullでなければ、「/」のルートとしてそれが使われる。
2. それ以外で`routes`テーブル（プロパティ）に「/」のエントリがあれば、それが使われる
3,4. ~省略~

つまり、今回の場合homeプロパティは指定されていないかつ、routesプロパティで「/」のエントリが設定されていたため、暗黙的に「/」のエントリが使われてしまったようです。（え、initialRouteなんのためにあるの、、）

## 修正後

ということで以下のようにinitialRouteを削除し、routesプロパティに「/」としてSplashPageを設定するように修正したところ意図した通りに動きました。

```entry.dart
    return MaterialApp(
      title: "NavigatorApp",
      routes: <String, WidgetBuilder>{
        '/': (BuildContext context) => Provider<SplashBloc>(  // '/'をSplashPageにする
              builder: (context) => SplashBloc(),
              child: SplashPage(),
              dispose: (_, bloc) {
                bloc.dispose();
              },
            ),
        '/welcome': (BuildContext context) => WelcomePage(),
        '/home': (BuildContext context) => HomePage(),
        '/settings': (BuildContext context) => SettingsPage(),
      },
    );
```

また、以下のようにhomeプロパティにSplashPageを定義しても良さそうです。

```entry.dart
    return MaterialApp(
      title: "NavigatorApp",
      home: Provider<SplashBloc>(  // '/'をSplashPageにする
              builder: (context) => SplashBloc(),
              child: SplashPage(),
              dispose: (_, bloc) {
                bloc.dispose();
              },
            ),
      routes: <String, WidgetBuilder>{
        '/welcome': (BuildContext context) => WelcomePage(),
        '/home': (BuildContext context) => HomePage(),
        '/settings': (BuildContext context) => SettingsPage(),
      },
    );
```

# まとめ

Navigator周りですが、ロジックが意外と複雑だったので、理解しておくと後でハマることがないので良さそうです。
また、initialRouteは今の所設定する理由がなさそうに見えますが、多分他に用途があると思うので、もう少し理解を深めていきたいです。
最後まで読んでいただきありがとうございました。