【Flutter】redirect を正しく扱うための go_router × Riverpod 設計ガイド
はじめに
Flutterアプリを開発する際、ユーザーのログイン状態やアプリのメンテナンス状態に応じて画面遷移を制御したい場面はよくあります。
このような場合に便利なのが、go_router
の redirect
機能です。
redirect
を使えば、現在のアプリの状態(例:ログイン済みかどうか、メンテナンス中かどうか)に応じて表示すべき画面を切り替えることができます。
しかし、この機能は一見シンプルに見えるものの、状態の監視・通知・再構築の流れを正しく理解しないと、うまく機能させるのが難しい側面もあります。
本記事では、この redirect
を Riverpod と組み合わせて、
- 明示的に状態を監視し、
- 状態変化時に
GoRouter
に通知し、 - 状況に応じた画面遷移を安全に実現する
という一連の仕組みを、実際の画面構成とルート設計を交えながら丁寧に解説していきます。
記事の対象者
- Flutterで
go_router
を使ったことがあるが、redirect
の使い方に不安がある方 - Riverpod を用いた状態管理に慣れてきた方
- 認証機能やメンテナンス機能を含むアプリを構築したい方
- 実際に状態に応じてルート構成を柔軟に切り替えたいと考えている方
記事を執筆時点での筆者の環境
[✓] Flutter (Channel stable, 3.29.0, on macOS
15.3.1 24D70 darwin-arm64, locale ja-JP)
[✓] Android toolchain - develop for Android
devices (Android SDK version 35.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.2)
[✓] VS Code (version 1.100.0)
サンプルプロジェクト
📝 今回の仕様
- アプリ起動後の初回画面は基本ログイン画面
- メンテナンスフラグが立っている場合はどんな状況でもメンテナンス画面にリダイレクトする
- ログインフラグがオンで、チュートリアル確認フラグがオフだったらチュートリアル画面にリダイレクトする
- ログインフラグがオン、チュートリアル確認フラグがオンだったらホーム画面にリダイレクトする
ログイン画面 ➡️ チュートリアル画面 ➡️ ホーム画面 ➡️ ログイン画面
チュートリアルをまだ確認していない場合のログイン、その後ログアウト
ログイン画面 ➡️ ホーム画面
すでにチュートリアルを確認している場合のログイン
設定画面 ➡️ メンテナンス画面 ➡️ ホーム画面
擬似的にメンテナスを発動して、5秒後に解除された場合
ログイン画面 ➡️ メンテナンス画面 ➡️ ログイン画面
擬似的にメンテナスを発動して、5秒後に解除された場合
ソースコード
補足
本記事は、筆者が以前執筆した記事で使用していたプロジェクトをベースに構成しています。
基本的なルートの構築方法や画面遷移に用いる関数の違いについては以下の記事をご覧ください。
redirect
の仕組み
実際の実装に入る前に redirect
の仕組みについて概要をつかんでいきましょう。
redirect
機能とは?
redirect
機能とは監視している状態の変化に応じて画面のルートを一度破棄して新しいルートを構築し直す機能です。
作り直したルートを再度ルーティングに送り直す、ということです。
例えばアプリのメンテナンス機能を考えてみます。
メンテナンス機能とはサーバーのメンテナンスやアプリの不具合を解消するためなどを理由に、アプリを一時的に使用不可にするための機能です。
この場合、通常のアプリの画面たちとメンテナンス中に表示する画面があったとします。
普段アプリを使う分にはメンテナンス画面には遷移させたくないので、通常画面群のルーティングには組み込まれていない独立した画面がメンテナンス画面です。
アプリをメンテナンス状態にするには、サーバーやFireaseのremote_configなどを使ってアプリに対してメンテナンスフラグを送信します。
そのフラグを監視して、その変化を検知することによって redirect
を実行し、メンテナンス画面へ遷移させるのです。
redirect
に必要な機構
この機能を動かすには大きく分けて4つの準備が必要です。
-
redirect
される画面とルート - 監視対象の状態とその状態を変える処理
- 状態の変更を検知して
redirect
を実行するように知らせる機能 - 状態によって構築すべきルートを出し分ける
redirect
機能
redirect
される画面とルートの準備
ここからは実際の構築方法を解説していきます。
まず初めに redirect
される画面とルートを準備します。
まず、以前の記事で紹介したアプリのルーティングではリダイレクトがありませんでした。
以下の図のように AppRoot
➡️ StatefulShellRoute
の一本道ルートでした。
今回の仕様ではそれぞれの画面が独立しているルートのため、以下のような構成になります。
画面の準備
画面は今回新たに追加された3画面を用意します。
ログイン画面
チュートリアル画面とメンテナンス画面
チュートリアル画面
メンテナンス画面
ルートの準備
上記で準備した画面をもとに各ルートを定義します。
ログインルート
チュートリアルルートとメンテナンスルート
チュートリアル画面
メンテナンス画面
各ルートを大元の route.dart
に定義します。
ここで注意が必要なのは AppShellRoute
の配下に LoginRoute
、 MaintenanceRoute
, TutorialRoute
を配置している点です。
今回の仕様ではそれぞれのルートは独立しています。例えば NavigationShellRoute
のホーム画面からログイン画面に直接遷移できないようにしているのです。
// このアノテーションはAppShellRouteにつける必要がある
<AppShellRoute>(
routes: [
TypedStatefulShellRoute<NavigationShellRoute>(
// ...
),
// ここから追加 ---->
TypedGoRoute<LoginRoute>(
path: LoginRoute.path,
name: LoginRoute.name,
),
TypedGoRoute<MaintenanceRoute>(
path: MaintenanceRoute.path,
name: MaintenanceRoute.name,
),
TypedGoRoute<TutorialRoute>(
path: TutorialRoute.path,
name: TutorialRoute.name,
),
// ここまで <----
],
)
/// アプリケーションの大元に位置するシェルルート。
class AppShellRoute extends ShellRouteData { ... }
ここまできたら一度自動生成コマンドを実行すれば画面とルートの準備は完了です。
flutter pub run build_runner build --delete-conflicting-outputs
余談: 設定画面からメンテナンス画面にはいけない
ここまで定義してみた場合に実験で設定画面からメンテナンス画面に遷移を試みてみましょう。
実際にはメンテナンス画面には遷移せず、ホーム画面にリダイレクトされてしまいます。
このことから NavigationShellRoute
にいる場合、 AppShellRoute
の配下にあるルートには遷移することができないことがわかります。
本来のアプリの挙動で意図しない遷移してしまう危険を防止することができます。
監視対象の状態とその状態を変える処理
今回の仕様でいう必要な状態管理は以下の三つです。
- ログイン状態
- チュートリアル確認状態
- メンテナンス状態
それぞれの状態の取り方は簡易的な実装となっています。
こちらは実際の本番アプリの場合はそれぞれに相応の実装に差し替えていただければと思います。
ログイン処理に伴う状態
今回はアプリを再起動する度にログアウト状態から始まっています。
本来はサーバーから返ってきたアクセストークンをローカルDBに保持して、その有無によって isLoggedIn
に流すような実装になると思われます。
ログイン状態は isLoggedInProvider
として配信します。
チュートリアルの確認状態の処理
今回はアプリ起動時に毎回チュートリアル未確認状態から始まっています。
本来であれば shared_preferences
などのローカルDBにチュートリアルを確認したかどうかの値を保存し、毎回保存した値を取り出して isTutorialChecked
に流す実装になると思われます。
チュートリアルの確認状態は isTutorialCheckedProvider
として配信します。
メンテナンス状態
setMaintenanceMode
を実行すると5秒後に自動でメンテナンスを解除するようにしています。
本来であればここはFirebaseのremote_configなどで流れてきた値をもとに isMaintenanceMode
として値を流すような実装になると思われます。
メンテナンス状態は isMaintenanceModeProvider
として配信します。
redirect
を実行するように知らせる機能
状態の変更を検知して ここからが主要な機能構築になっています。
ログイン状態、チュートリアル確認状態、メンテナンス状態を監視して、変更を検知したらしたら GoRouter
に知らせる機能を作ります。
go_router
パッケージ内ではこの機能を有したオブジェクトである Listenable
を refreshListenable
という引数に渡す必要があります。
この Listenable
を riverpod
で扱うにはそれぞれの知識が必要で複雑なので順番に解説していきます。
Listenable
とは?
Listenable
は「通知を送ることができるオブジェクト」を定義するための抽象クラス(インターフェース)です。
abstract class Listenable { ... }
📌 なにができるの?
• リスナー(listener)を登録 できる
• 変更があったときにリスナーに通知 できる
例えば、ボタンを押したら何かが変わる → 画面を更新したい、というような場面で使われます。
🌱 Listenable には2つの派生系がある
1. ValueListenable
• 「通知 + 値を持つ」タイプ。
• 今の値(current value) を取得できる。
• 例: ValueNotifier<String>('Hello')
2. Animation
• ValueListenable をさらに拡張して、「アニメーションの方向(前進 or 戻る)」の概念を追加。
• アニメーションに特化した通知ができる。
今回の refreshListenable
では ValueListenable
が関係してきます。
abstract class ValueListenable<T> extends Listenable { ... }
🔧 よく使う実装クラス
⭐️ ChangeNotifier
• Listenable を実装する代表的なクラス。
• notifyListeners() で登録されたすべてのリスナーに通知。
• Provider や setState による状態管理の基盤にもなる。
⭐️ ValueNotifier<T>
• ValueListenable を実装。
• value を変えると、自動で通知される。
• 状態管理がとても簡単にできるので、軽量なアプリでよく使われる。
今回の refreshListenable
では ValueNotifier
が関係してきます。
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> { ... }
riverpod
で ValueNotifier
を返却する provider
を実装するには?
上記の流れを汲んでいくと、 ValueNotifier
で変更を流せばいいんだなということがわかりました。
これを riverpod
で実装するには一工夫が必要です。ただ単に ValueNotifier<T>
を返す provider
ではうまくいきません。
この件に関しては riverpod
の公式ドキュメントにも対応策が記載されています。
riverpod
の提供する Raw
型を使って Raw<ValueNotifier<T>>
を返却するようにします。
🔍 Raw<T>
とは?
Raw<T>
は 「Riverpod に対して、この型は特別な扱いをしなくていいよ」 という意図を示すための 型エイリアス(typedef
)です。
typedef Raw<T> = T;
→ つまり、実行時には何の影響もない(Tそのもの)けれど、コード生成時やLinterの警告を避ける目的で使うものです。
🔧 主な使い道
① AsyncValue に自動変換されたくないとき
通常、非同期処理を返すとRiverpodが AsyncValue<T> にラップします。
Future<int> myProvider(...) async => 42;
// ⇒ ref.watch(myProvider) の戻り値は AsyncValue<int>
ただし、ラップされたくないときは Raw<Future<int>>
を使います。
Raw<Future<int>> myProvider(...) async => 42;
// ⇒ ref.watch(myProvider) は Future<int> のまま!
② Linterに「この型は意図的に使っている」と伝える
例えば ChangeNotifier を返すと、通常はLinterに警告されます。
MyNotifier myProvider(...) => MyNotifier(); // ❌ 警告
そんなときは以下のように書くことで警告を回避できます:
Raw<MyNotifier> myProvider(...) => MyNotifier(); // ✅ OK
refreshListenableProvider
を実装する
ここまでの解説を踏まえて、各種の状態を監視して変更を検知した際にその変更を通知するためのprovider
を以下のように定義します。
(keepAlive: true)
Raw<ValueNotifier<int>> refreshListenable(Ref ref) {
final notifier = ValueNotifier<int>(0);
void notify() {
// 安全なタイミングまでちょっと待つ
Future.microtask(() {
// ValueNotifierの値を更新して通知をトリガー
notifier.value++;
});
}
// 各状態が変わったときに value を更新して通知をトリガー
final maintenanceInfoSub =
ref.listen(isMaintenanceModeProvider, (_, __) => notify());
final isLoggedInSub = ref.listen(isLoggedInProvider, (_, __) => notify());
final isTutorialCheckedSub =
ref.listen(isTutorialCheckedProvider, (_, __) => notify());
// 万が一、このProviderが破棄された時のクリーンアップ処理
ref.onDispose(() {
notifier.dispose();
isLoggedInSub.close();
maintenanceInfoSub.close();
isTutorialCheckedSub.close();
});
// ValueNotifierが変更されたときにref.notifyListeners()で依存Providerに通知
notifier.addListener(() {
ref.notifyListeners();
if (kDebugMode) {
print('⭐️⭐️⭐️ notifier value ${notifier.value} ⭐️⭐️⭐️');
}
});
return notifier;
}
👀 状態を監視と通知
ref.listen
で各種状態を監視しています。
その値に何らかの変更があった場合は notify()
を実行して変更を通知しています。
🛠️ 通知は少し待機
notify()
では単純に notifier.value++;
としているだけですが、 Future.microtask
で少し待機させないと変更通知がうまく伝播されないことがあるので入れています。
Future.microtask
を使う理由は、ref.listen
による変更検知の直後は Riverpod 側の内部状態がまだ安定していない場合があるためです。
すぐに .value++
を実行すると通知が正しく伝播されないことがあります。microtask
によって1フレーム先送りすることでこれを回避できます。
🗑️ 廃棄時の安全処理
この provider
は基本的にアプリが起動されている間は破棄されてほしくはないので keepAlive: true
としています。
万が一破棄された場合に備えて、ref.listen で作成されたサブスクリプションを明示的に close() する処理も追加しています。
📲 通知を送信する
最後の方で入れている ref.notifyListeners();
で変更を通知しています。この通知先はつまりは GoRouter
インスタンスを生成している provider
となります。
redirect
機能
状態によって構築すべきルートを出し分ける 最後に変更を検知したらどのルートを出し分けるかのを決めて返却する機能を定義します。
こちらは provider
を使って依存性注入できるようにした RedirectController
クラスに定義していきます。
(keepAlive: true)
RedirectController redirectController(Ref ref) => RedirectController(ref);
class RedirectController {
RedirectController(this.ref);
final Ref ref;
}
redirect
を行う関数の全体像
GoRouter
の引数に入れる redirect
関数は以下のような定義となっています。
/// The signature of the redirect callback.
typedef GoRouterRedirect = FutureOr<String?> Function(
BuildContext context, GoRouterState state);
それを踏まえた RedirectController
で定義する関数は以下のとおりです。
クラス名により実行処理は自明なので、命名は call
としました。
引数において今回はBuildContext context
は使わないので GoRouterState state
のみとしました。
そして、この関数は最終的に String?
を返さなければいけません。
返却すべき値はリダイレクトすべきルートのパスです。
/// リダイレクト処理を行う
FutureOr<String?> call(GoRouterState state) async {
// 現在のパスを取得
final fullPath = state.fullPath;
// メンテナンスモードの状態を取得
final isMaintenanceMode = await ref.read(isMaintenanceModeProvider.future);
// メンテナンスモードの場合はメンテナンス画面へリダイレクト
if (isMaintenanceMode) return _maintenanceGuard(fullPath);
// ログイン状態を取得
final isLoggedIn = await ref.read(isLoggedInProvider.future);
// ログインしているかどうかでリダイレクト先を決定
if (isLoggedIn) return _authGuard(fullPath);
// ログインしていない場合のリダイレクト処理
return _noAuthGuard(fullPath);
}
大きな流れは以下のとおりです。
① 現在のパスを取得
GoRouterState
にはリダイレクト前のアプリが現在表示している画面のルートがパスとして保持されています。
その現在地を使って各状態によってリダイレクトする画面(ルート)をどこにするのかを決定していきます。
② メンテナンス状態の取得とハンドリング
isMaintenanceModeProvider
の状態を取得して、もしもメンテナンス中であれば _maintenanceGuard
内でリダイレクト先を返します。
③ ログイン状態の取得とハンドリング
isLoggedInProvider
の状態を取得して、もしもログイン中であれば _authGuard
内でリダイレクト先を返します。
④ ログインしていない場合のハンドリング
メンテナンス中でもなく、ログイン中でもない場合は _noAuthGuard
内でリダイレクト先を返します。
_maintenanceGuard
リダイレクトの動き ここでは _maintenanceGuard
の動きを見ていきましょう。
/// メンテナンスモードの場合のリダイレクト処理
String? _maintenanceGuard(String? fullPath) {
// メンテナンスモード中は全ての画面をメンテナンス画面にリダイレクト
if (fullPath != const MaintenanceRoute().location) {
return const MaintenanceRoute().location;
}
return null;
}
この関数内に入ってきた時点ですでにメンテナンス状態であることは確定しています。
やっていることは現在のパスがメンテナンス画面のルートかどうかを判定し、まだメンテナンス画面にいないのであれば MaintenanceRoute().location;
を返します。
それで終わりかというとそうではありません。
ここで MaintenanceRoute().location;
をリターンした後に実はもう一度 call
関数が呼び出されます。
GoRouter
のreidirect
関数は、null
が返されるまで反復処理します。
なので、一度目のリダイレクトが終わってもう一度実行された場合に再び call
が呼ばれ、メンテナンス中かどうかを判定し、
_maintenanceGuard
まで到達します。
そこで今度は fullPath != const MaintenanceRoute().location
が false
となっているので、 return null;
が返ります。
ここでようやく、リダイレクト処理は終了します。
その他のリダイレクト関数も基本的な作りは _maintenanceGuard
と同じです。
その他のプライベート関数
/// ログインしている場合のリダイレクト処理
Future<String?> _authGuard(String? fullPath) async {
final isTutorialChecked = await ref.read(isTutorialCheckedProvider.future);
// チュートリアルが未完了の場合はチュートリアル画面へリダイレクト;
if (!isTutorialChecked) return _tutorialGuard(fullPath);
// チュートリアルが完了している場合はホーム画面へリダイレクト
if (fullPath == const LoginRoute().location ||
fullPath == const MaintenanceRoute().location ||
fullPath == const TutorialRoute().location) {
return const HomeRoute().location;
}
return null;
}
/// チュートリアルが完了していない場合のリダイレクト処理
String? _tutorialGuard(String? fullPath) {
// チュートリアルが未完了の場合はチュートリアル画面へリダイレクト
if (fullPath != const TutorialRoute().location) {
return const TutorialRoute().location;
}
return null;
}
/// ログインしていない場合のリダイレクト処理
String? _noAuthGuard(String? fullPath) {
if (fullPath != const LoginRoute().location) {
return const LoginRoute().location;
}
return null;
}
GoRouter
に refreshListenable
と redirect
を設定する
最後に今までに定義してきた 状態の変更を検知して redirect
を実行するように知らせる機能 である refreshListenableProvider
と 状態によって構築すべきルートを出し分ける redirect
機能 を設定すれば完了です。
(keepAlive: true)
GoRouter appRouter(Ref ref) {
return GoRouter(
// ...
refreshListenable: ref.read(refreshListenableProvider), // 💡💡💡
redirect: (_, state) => ref.read(redirectControllerProvider).call(state), // 💡💡💡
// ...
);
}
終わりに
本記事では、go_router
の redirect
機能と Riverpod を組み合わせて、アプリの状態に応じた柔軟な画面遷移を実現する方法を詳しく解説しました。
特に以下のポイントを中心に構成しました:
-
redirect
の動作原理とその再評価のタイミング - 状態変更を通知するための
refreshListenable
の仕組み - Riverpodで
Listenable
を扱うためのRaw<ValueNotifier>
の活用 - アプリの設計意図をルート設計にどう落とし込むか
単なるテクニックにとどまらず、「なぜこの設計にするのか」を意識して実装していくことで、より保守性・拡張性の高いアプリを構築できるはずです。
この記事が、redirect
の仕組みを正しく理解し、実用的に導入していくための一助となれば幸いです。
Discussion