state.pageKeyの内部実装を見てみる
UdemyでGoRouterの講座をやってみたのだが、間違っている箇所があった😅
そもそもルートがネストしてないとエラー出るのだが。。。
動画が少し古いのだろう。バージョンアップするとパスの設定は変わるんですよね。
リファクタリングしたので書いてあるソースコードでどんな役割なのかわからない以下のコードをこのあと調べる。
- state.pageKey
- state.pageKey.value
リファクタリングしたのでエラーは解消できた。
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:udemy_go_router/detail_page.dart';
import 'package:udemy_go_router/home_page.dart';
import 'package:udemy_go_router/login_page.dart';
import 'package:udemy_go_router/sessions.dart';
import 'package:udemy_go_router/splash_page.dart';
/// The route configuration.
final GoRouter router = GoRouter(
routes: <RouteBase>[
/// TOP LEVEL PATH
GoRoute(
path: '/',
name: 'splash',
builder: (BuildContext context, GoRouterState state) => const SplashPage(),
),
/// TOP LEVEL PATH
GoRoute(
path: '/login/redirection',
name: 'login-redirection',
redirect: (BuildContext context, GoRouterState state) async {
if (await checkedLoggedIn()) {
return '/home';
} else {
return '/login';
}
}),
/// Page
GoRoute(
path: '/home',
name: 'home',
pageBuilder: (context, state) => NoTransitionPage<void>(
key: state.pageKey,
restorationId: state.pageKey.value,
child: const HomePage(),
),
routes: [
GoRoute(
path: 'detail/:id', // /home/detail/:id となる
name: 'detail',
builder: (context, state) => DetailPage(
id: state.pathParameters['id']!,
),
),
],
),
/// Login
GoRoute(
path: '/login',
name: 'login',
pageBuilder: (context, state) => NoTransitionPage<void>(
key: state.pageKey,
restorationId: state.pageKey.value,
child: const LoginPage(),
),
// builder: (context, state) => const LoginPage(),
),
],
);
Name Route
モバイルアプリで画面遷移するときは、context.goNamed
にしないとエラーが発生する。Flutter Webだと公式は名前つきルートを使うのを推奨していないそうだが、意外と現場ではやってるのを見かける。Webでやるときは、context.go
にすれば良いと思う。
ネストしたルートだけの気もするが。。。
パスパラメーターを使用する必要があったのでコードも修正した。
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:udemy_go_router/sessions.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ホーム'),
actions: [
TextButton(onPressed: () async {
await logout();
if(context.mounted) {
context.go('/login');
}
}, child: const Text('ログアウト'))
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(onPressed: () {
// ✅ 正しい使用方法
context.pushNamed(
'detail',
pathParameters: {'id': 'A'}
);
}, child: const Text('Aを閲覧する')),
ElevatedButton(onPressed: () {
// ✅ 正しい使用方法
context.pushNamed(
'detail',
pathParameters: {'id': 'B'}
);
}, child: const Text('Bを閲覧する')),
ElevatedButton(onPressed: () {
// ✅ 正しい使用方法
context.pushNamed(
'detail',
pathParameters: {'id': 'C'}
);
}, child: const Text('Cを閲覧する')),
],
),
),
);
}
}
state.pageKey, state.pageKey.value
内部実装によるとこのようなことが書いてあった。
What state.pageKey?
/// このサブルートのユニークな文字列キー。
/// 例えば
///dart /// ValueKey('/family/:fid') ///
サブルートにユニークな文字列キーが渡されるようだ。
/// See also:
///
/// * [Widget.key], which discusses how widgets use keys.
class ValueKey<T> extends LocalKey {
/// Creates a key that delegates its [operator==] to the given value.
const ValueKey(this.value);
/// The value to which this key delegates its [operator==]
final T value;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is ValueKey<T>
&& other.value == value;
}
@override
int get hashCode => Object.hash(runtimeType, value);
翻訳したコード
/// こちらもご覧ください:
///
ウィジェットがキーを使用する方法については、 /// * [Widget.key] も参照してください。
class ValueKey<T> extends LocalKey { /// 与えられた値に [operator==] を委譲するキーを作成します。
/// 与えられた値に [operator==] を委譲するキーを作成します。
const ValueKey(this.value);
/// このキーが [operator==] を委譲する値。
final T value;
オーバーライド
bool operator ==(Object other) { 次のようになります。
if (other.runtimeType != runtimeType) { if (other.runtimeType != runtimeType)
return false;
}
return other is ValueKey<T>
&& other.value == value;
}
オーバーライド
int get hashCode => Object.hash(runtimeType, value);
委譲するキーなるものの値なようだ。
なぜstate.pageKey, state.pageKey.valueが必要なのかというと、pageBuilderを使用し、NoTransitionPageを使用するからである。
/// トランジションなしのカスタムトランジションページ。
class NoTransitionPage<T> extends CustomTransitionPage<T> {
/// 遷移機能を持たないページのコンストラクタです。
/// トランジション機能を持たないページのコンストラクタです。
const NoTransitionPage({
required super.child,
super.name,
super.arguments,
super.restorationId,
super.key,
}) : super(
transitionsBuilder: _transitionsBuilder,
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
);
static Widget _transitionsBuilder(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) =>
child;
}
NoTransitionPageを使用する主な理由について説明します:
- アニメーションの制御
- NoTransitionPageは、ページ遷移時のアニメーションを無効化します
- デフォルトのMaterialPageTransitionsBuilderによる遷移アニメーションを避けたい場合に使用します
- 特に以下のような場合に有効です:
- スプラッシュ画面からホーム画面への遷移
- ログイン画面からメイン画面への遷移
- 即時的な画面切り替えが必要な場合
- パフォーマンスの最適化
pageBuilder: (context, state) => NoTransitionPage<void>(
key: state.pageKey,
restorationId: state.pageKey.value,
child: const HomePage(),
)
- アニメーションを省略することで、遷移時のパフォーマンスが向上します
- 特に複雑なUIや大量のデータを扱う画面での遷移時に効果的です
- ユーザー体験の考慮
- 特定のケースでは、アニメーションなしの即時遷移の方が適切な場合があります:
- 認証フロー
- エラー画面への遷移
- モーダル表示
- アプリの状態管理
restorationId: state.pageKey.value
- restorationIdを指定することで、アプリの状態復元をサポートします
- アプリが中断から再開された際の画面状態の保持に役立ちます
ただし、以下の点に注意が必要です:
- すべてのルートでNoTransitionPageを使用する必要はありません
- ユーザーの操作に基づく通常の画面遷移では、適切なアニメーションを使用することでUXが向上する場合もあります
- アプリの性質や要件に応じて、使い分けを検討してください
例えば、DetailPageではbuilderを使用していますが、これは通常の遷移アニメーションが適していると判断されているためだと考えられます:
GoRoute(
path: RoutePath.detailId,
name: 'detail',
builder: (context, state) => DetailPage(
id: state.pathParameters['id']!,
),
)
まとめ
画面遷移のアニメーションを無効化したいときに使うものでした。昔ボトムナビゲーションバーとリダイレクトの処理を組み合わせると発生したエラーを解決するときに使ったことあったような。