Historical source · 2021–2022
日本語アーカイブ · リダイレクト
原文・コード例・画像を保存した2021–2022年のドキュメント。
On this page
リダイレクト
go_router はルートからルートへアプリをリダイレクトすることができます。 ナビゲーションイベントが発生するごとにトップレベル(ルータレベル)でリダイレクトを行うことも可能ですし、特定のルートにてルートレベルで行うことも可能です。
トップレベル(ルータレベル)におけるリダイレクト
ユーザーがまだログインしていないときなど、その時点で表示されるべきではないページへのアクセスを回避したいことはありませんか。 例えば、ユーザーのログイン情報を管理するクラスがあるとします。
class LoginInfo extends ChangeNotifier {
var _userName = '';
String get userName => _userName;
bool get loggedIn => _userName.isNotEmpty;
void login(String userName) {
_userName = userName;
notifyListeners();
}
void logout() {
_userName = '';
notifyListeners();
}
}このログイン情報を GoRouter コンストラクタの redirect 関数で使用します。
class App extends StatelessWidget {
final loginInfo = LoginInfo();
...
late final _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(families: Families.data),
),
...,
GoRoute(
path: '/login',
builder: (context, state) => LoginScreen(),
),
],
// ユーザーがログインしていない場合はログインページにリダイレクト
redirect: (state) {
// ユーザーがログインしていない場合はログインが必要
final loggedIn = loginInfo.loggedIn;
final loggingIn = state.subloc == '/login';
if (!loggedIn) return loggingIn ? null : '/login';
// ユーザーがログインしているにも関わらず、まだログイン画面にいる場合は
// ホーム画面に誘導する
if (loggingIn) return '/';
// 一切リダイレクトが不要な場合
return null;
},
);
}このコードでは、ユーザーがログインしていない場合は、null を返すことで元々行く予定の /login にそのまま誘導するか、 '/login' を返すことで /login にリダイレクトします。
一方、ユーザーが既にログインしているにも関わらず /login に向かっている場合は、代わりに / へリダイレクトします。
そして、ユーザーが既にログインしており /login 以外のロケーションへ向かっている場合は、null を返すことで元々の行き先に誘導します。 redirect 関数は null を返すまで繰り返し呼び出され、連続リダイレクト します。
このようなログイン情報を、ウィジェットツリーに挿入してアプリ内のどこからでもアクセスできるようにするには、 provider のような状態管理パッケージの利用をおすすめします。
class App extends StatelessWidget {
final loginInfo = LoginInfo();
// ログイン情報をアプリの状態としてウィジェットツリーに挿入
@override
Widget build(BuildContext context) => ChangeNotifierProvider<LoginInfo>.value(
value: loginInfo,
child: MaterialApp.router(...),
);
...
}ウィジェットツリーにログイン情報があることで、ログイン画面を簡単に実装することができます。
class LoginScreen extends StatelessWidget {
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text(_title(context))),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
// ユーザーをログインさせてリスナーに通知
context.read<LoginInfo>().login('test-user');
// ホーム画面へ
context.go('/');
},
child: const Text('Login'),
),
],
),
),
);
}上記コード例ではユーザーをログインさせた後、更にコードを加えてホーム画面にリダイレクトしています。 これは go_router がルートに影響するような形で、アプリの状態が変化した(ログインした)ことを知る術がないからです。 アプリの状態の変化によって go_router を自動でリダイレクトさせる場合は GoRouter コンストラクタの refreshListenable 引数を使用してください。
class App extends StatelessWidget {
final loginInfo = LoginInfo();
...
late final _router = GoRouter(
routes: ...,
redirect: ...
// listenable に変化があればルータによってルートがリフレッシュされる
refreshListenable: loginInfo,
);
}loginInfo は ChangeNotifier なので、変更があればそのリスナーに通知されます。 これを GoRouter コンストラクタに渡すことで、go_router はログイン情報の変更を以って、自動的にルートを更新します。 これによりアプリのログインロジックをシンプルにすることができます。
onPressed: () {
// ユーザーをログインさせてリスナーに通知
context.read<LoginInfo>().login('test-user');
// ルータが refreshListenable を利用して /login から / へ自動的にリダイレクトする
//context.go('/');
},アプリの状態が変わった際のルートの処理を自動で行うために、 トップレベルの redirect を使う際は refreshListenable も同時に使用することをおすすめします。
Stream でルートを更新する
ルートの決定に関わる状態を Listenable ではなく Stream で管理している場合は、GoRouterRefreshStream を Stream のラッパーとして使用してください。 これにより、BLoC のようなストリームベースの状態管理ソリューションにおいても同様のことが可能になります。
class App extends StatelessWidget {
final streamController = StreamController();
...
late final _router = GoRouter(
routes: ...,
redirect: ...
// listenable の変化に応じて、ルータが自身のルートを更新
refreshListenable: GoRouterRefreshStream(streamController.stream),
);
}ルートレベルにおけるリダイレクト
GoRouter コンストラクタで設定するトップレベルのリダイレクトハンドラは、 アプリの現在の状態を元にナビゲーションイベントを処理したい場合に便利です。 しかし、特定のルート (サブルート) のみで行われるようなリダイレクト処理を設定したい場合はどのようにしたらいいでしょうか。 この場合は GoRoute コンストラクタで redirect 関数を設定してください。
final _router = GoRouter(
routes: [
GoRoute(
path: '/',
redirect: (_) => '/family/${Families.data[0].id}',
),
GoRoute(
path: '/family/:fid',
builder: ...,
],
);上記コード例では、ユーザーが / に遷移すると redirect 関数が呼び出され、Families の最初のデータを使用して /family/:id ルートにリダイレクトされます。 リダイレクトはサブルートが全てマッチしてから発生するため、別ページへの遷移中に、ロケーション名をパースしているところでリダイレクトが行われる心配はありません。
考慮すべき点
ルートレベルのリダイレクトを使用する際、いくつか考慮すべき点があります。 まず、特定の条件下でのみリダイレクトを行うような redirect 関数を持つ場合は、builder(pageBuilder)関数を併用する必要があります。
GoRoute(
path: '/',
redirect: (_) => kGoElsewhere ? '/elsewhere' : null,
builder: (context, state) => ..., // kGoElsewhere == false の場合必要
)この場合、builder 関数はリダイレクトの条件が false のときに呼び出されます。
また、リダイレクト先がサブルートの場合においても builder(pageBuilder)関数が必要になります。
GoRoute(
path: '/profile',
redirect: (_) => '/profile/home', // 行き先が /profile のときに呼び出される
builder: (c, s) => ..., // /profile/:section のページスタックを生成するために必要
routes: [
GoRoute(
path: ':section',
builder: ...ProfileScreen(state.params['section']!)...,
),
],
)redirect 関数が呼び出されるのは、そのルートが遷移先の終着点であるときに限られます。 それ以外のルートに redirect が設定されている場合は無視され、代わりにスタック用のページを生成するための builder(pageBuilder)が呼び出されます。
実際、ルートの途中で redirect 関数を使用するようなケースはおそらくないでしょう。 遷移先の終着点で呼び出され、別のトップレベル(GoRouter.routes 直下)ルートにリダイレクトする場合がほとんどかと思います。
GoRoute(
path: '/profile',
redirect: (_) => '/profile/home', // この場合 builder は必要ありません
),
GoRoute
path: '/profile/:section',
builder: ...ProfileScreen(state.params['section']!)...,
)また、これはある意味当然ですが、ルートレベルに redirect 関数が全くない場合は builder(pageBuilder)は必須です。
パラメータを伴うリダイレクト
パスがパラメータ化されているケースでは、そのパラメータを考慮に入れてリダイレクトを行いたいことがあります。 その場合は、redirect 関数に渡される state オブジェクトの params 引数を使用してください。
GoRoute(
path: '/author/:authorId',
redirect: (state) => '/authors/${state.params['authorId']}',
),連続リダイレクト
単一のナビゲーションイベントから連続で複数のリダイレクトを行うことも可能です。(例: / => /foo => /bar) これは時間の経過とともにルートを積み重ねることができ、各ルートへ直接遷移するためのロケーション名を気にかける必要がない点で便利です。 さらに、トップレベルとルートレベルのリダイレクトは何通りにも組み合わせることが可能です。
リダイレクトが何度も連続で行われた場合は、アプリにバグが発生している可能性が高いことを示しています。 そのため、デフォルトでは5回のリダイレクトで例外が発生する設定になっています。 これは GoRouter コンストラクタの redirectLimit で任意の数字に変更することが可能です。
その他起こりうるトラブルは、/ => /foo => / などループが発生することです。 この場合は例外を受け取ることができます。
具体例: リダイレクトとクエリパラメータ
ディープリンクを使ってユーザーにページを表示する前に、ログインさせたい場合があります。 その場合は、リダイレクト関数の中でクエリパラメータを使用することで元々の行き先をリダイレクト先に伝えることができます。
class App extends StatelessWidget {
final loginInfo = LoginInfo();
...
late final _router = GoRouter(
routes: ...,
// ユーザーがログインしていない場合はログインページにリダイレクト
redirect: (state) {
// ユーザーがログインしていない場合はログインが必要
final loggedIn = loginInfo.loggedIn;
final loggingIn = state.subloc == '/login';
// ユーザーの元々の行き先のロケーション名をクエリパラメータに紐づける
final fromp = state.subloc == '/' ? '' : '?from=${state.subloc}';
if (!loggedIn) return loggingIn ? null : '/login$fromp';
// ユーザーがログインしている場合はユーザーを元々の行き先に誘導する
// 行き先がない場合はホーム画面へ
if (loggingIn) return state.queryParams['from'] ?? '/';
// 一切リダイレクトが不要な場合
return null;
},
// listenable に変化があればルータによってルートがリフレッシュされる
refreshListenable: loginInfo,
);
}この例では、ユーザーがログインしていない場合はクエリパラメータ from に元々のディープリンクをセットし、/login にリダイレクトします。
from パラメータの値は state オブジェクトの location か subloc から生成することができます。 location と subloc の違いは、そのロケーションにおけるクエリパラメータを含むか含まないかです。 ここではシンプルさを優先してクエリパラメータを含まない subloc を選択しています。 ただ、クエリパラメータは URL エンコードされるため location を使用しても問題ありません。ここはディーラーズ・チョイスです。
ユーザーがログインするとルータによるルート更新のトリガーが発動し、その結果 redirect が呼び出されます。 この時点ではユーザーはログインしている状態にも関わらず /login ロケーションにいるため、 クエリパラメータ from にセットされている行き先か、from が空であれば / に誘導します。
それだけです。redirect 関数と refreshListenable の組み合わせでは、ログイン画面は from の値さえ知る必要はないのです。 リダイレクトが続く間は from のロケーション名が渡され、必要がなくなれば削除されるだけです。朝飯前ですね。
これと同じテクニックはログイン画面以外にも応用できます。 詳しくはローディング画面のサンプルをご覧ください。
Sources & version context
GoRouter.dev documentation edition. The preserved text describes the original package APIs. Follow the current guide before using it in a new project.