久々に見直してたら、@TypedGoRoute
を使って、
パスパラメタとかも型を意識して呼び出せるようになってた。
いろいろ忘れているので、再度入門したときの備忘録(*´ω`*)
インストール
$ fvm flutter pub add go_router # buider系も追加 $ fvm flutter pub add -d go_router_builder build_runner build_verify
pubspec.yaml
はこんな感じ。
# pubspec.yaml environment: sdk: ">=3.0.5 <4.0.0" dependencies: flutter: sdk: flutter go_router: ^9.0.3 dev_dependencies: flutter_test: sdk: flutter go_router_builder: ^2.2.0 build_runner: ^2.4.6 build_verify: ^3.1.0 # hooks / riverpod系 hooks_riverpod: ^2.3.6 riverpod_annotation: ^2.1.1 flutter_hooks:
ルートの準備
// routes.dart import 'dart:async'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; // ...略 part "routes.g.dart"; // ******************************************************** // * Entrypoints // * パスは定数にしてまとめておく // ******************************************************** class AppRoutes { static const splashPage = "/splash"; static const homePage = "/"; static const userPage = "users/:uid"; } // ******************************************************** // * RouteData // * GoRouteDataをそれぞれ設定 // ******************************************************** // TOPレベルのパスには、@TypedGoRouteをつける @TypedGoRoute<SplashPageRoute>(path: AppRoutes.splashPage) class SplashPageRoute extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) => const SplashPage(); } // ネストしたルートがある場合は、`@TypedGoRoute.routes`に記載 @TypedGoRoute<HomePageRoute>( path: AppRoutes.homePage, routes: [ TypedGoRoute<UserPageRoute>(path: AppRoutes.userPage), ], ) class HomePageRoute extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) => const HomePage(); } // TOPレベルでない場合は、`@TypedGoRoute`をつけない @immutable class UserPageRoute extends GoRouteData { const UserPageRoute({required this.uid}); final int uid; @override Widget build(BuildContext context, GoRouterState state) => UserPage(uid: uid); }
routerの準備
どこでも取得できるようにRiverpodでProvider化。
import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; // さっきのルートの一覧 import 'routes.dart'; part "router.g.dart"; final GlobalKey<NavigatorState> _rootNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'root'); @riverpod class AppRouter extends _$AppRouter { @override GoRouter build() { return GoRouter( navigatorKey: _rootNavigatorKey, debugLogDiagnostics: kDebugMode, initialLocation: AppRoutes.splashPage, routes: $appRoutes, ); } }
最後にMaterialApp.router
に設定する。
// app.dart import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'router.dart'; class App extends HookConsumerWidget { const App({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final router = ref.watch(appRouterProvider); return MaterialApp.router( title: "My Sample App", // go_routerの設定 restorationScopeId: 'router', routerDelegate: router.routerDelegate, routeInformationProvider: router.routeInformationProvider, routeInformationParser: router.routeInformationParser, ); } }
これで、各Routeのクラスが作成されて、
こんな感じで呼び出せるようになる。
TextButton( onPressed: () => const UserPageRoute(uid: 1).go(context), child: const Text('to User 1'), )
Webでパスから#
をなくす
いつのまにか設定方法が変わってた。。
flutter_web_plugins
のurl_strategy
を使う
$ fvm flutter pub add flutter_web_plugins --sdk=flutter
# pubspec.yaml dependencies: flutter: sdk: flutter flutter_web_plugins: sdk: flutter
あとはrunApp()
の前にusePathUrlStrategy()
を呼べばOK
// main.dart import 'dart:async'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; import 'app.dart'; FutureOr<void> main() async { // go_routerの設定 usePathUrlStrategy(); runApp(const ProviderScope(child: App())); }
ShellRouteでAppBarなどを固定する
このままだと、ページ遷移したときに、
AppBarはBottomNavigationBarもスライドされてしまう。。
ShellRouteを使うとそれらを固定できるよう。
使い方はこんな感じ。レイアウト的なこともこれでできそう。
import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'routes.dart'; part "router.g.dart"; final GlobalKey<NavigatorState> _rootNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'root'); // ShellRoute用のKeyを追加 final GlobalKey<NavigatorState> _shellNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'shell'); @riverpod class AppRouter extends _$AppRouter { @override GoRouter build() { // 現在のlocationを取得 final current = GoRouterState.of(context).location; return GoRouter( navigatorKey: _rootNavigatorKey, debugLogDiagnostics: kDebugMode, initialLocation: AppRoutes.splashPage, routes: <RouteBase>[ // ShellRouteを追加 ShellRoute( navigatorKey: _shellNavigatorKey, builder: (BuildContext context, GoRouterState state, Widget child) { // ShellRouteで、AppLayout的なScaffoldを返す return Scaffold( appBar: AppBar( // ShellRoute内だと、戻るボタンが表示されないので処理を追加 leading: _showLeading(context) ? _leadButton(context) : null, title: Text("My Sample App"), ), body: Center(child: child), ); }, // 自動生成した元のroutesを設定 routes: $appRoutes, ), ], ); } // 戻るボタン Widget _leadButton(BuildContext context) { return GestureDetector( onTap: () => context.pop(), child: const Icon(Icons.arrow_back), ); } // 戻るボタンを表示するかの判定 bool _showLeading(BuildContext context) { return ![ AppRoutes.homePage, AppRoutes.splashPage, ].contains(GoRouterState.of(context).location); } }
戻るボタンに関しては、これを参考にした感じ
ListenableでGoRouterにredirectを依頼する
GoRouterのredirect
に処理を書いた場合でも、
パスの変更が発生しないと処理されない。
認証状態などが変化した場合に、redirect
を実行してほしかったりする。
そういった場合は、Listenable
を用意して、
GoRouter.refreshListenable
を使うとよいらしい。
riverpodのgo_router+firebase_authのサンプルがわかりやすい。
ただドキュメントにも詳細がないので、かなり苦労した。。
Listenable
はこんな感じ。riverpodを利用。
// router_listenable.dart import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'providers.dart'; import 'routes.dart'; part 'router_listenable.g.dart'; @riverpod class RouterListenable extends _$RouterListenable implements Listenable { VoidCallback? _routerListener; int _count = 0; // 他のProviderの値を監視する部分 @override Future<void> build() async { // 10からカウントダウンするProviderの値を監視 _count = ref.watch(countDownProvider); // ここで自分自身を監視。`_count`など`state`の値が変わったら、 // GoRouterにredirectの処理を依頼する(=`_routerListener?.call()`) ref.listenSelf((prev, next) { // stateがloadingの場合は無視する if (state.isLoading) return; // oRouterにredirectの処理を依頼 _routerListener?.call(); }); } // リダイレクトする条件をまとめておく部分 String? redirect(BuildContext context, GoRouterState state) { // loading中やエラーがあるときは無視する if (this.state.isLoading || this.state.hasError) return null; // locationのパスやstateの値を条件を判定 if (state.location == AppRoutes.splashPage && _count == 0) { // リダイレクト先のパスを返す return AppRoutes.homePage; } // リダイレクトしない場合はnullを返す return null; // GoRouterから渡されるcallbackの設定 @override void addListener(VoidCallback listener) => _routerListener = listener; // GoRouterから渡されたcallbackの削除 @override void removeListener(VoidCallback listener) => _routerListener = null; }
次にGoRouter側の設定を変更する。
import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'routes.dart'; // 追加 import 'router_listenable.dart'; part "router.g.dart"; final GlobalKey<NavigatorState> _rootNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'root'); @riverpod class AppRouter extends _$AppRouter { @override GoRouter build() { // さっきのListenableを取得 final routerListenable = ref.watch(routerListenableProvider.notifier); return GoRouter( navigatorKey: _rootNavigatorKey, debugLogDiagnostics: kDebugMode, initialLocation: AppRoutes.splashPage, routes: $appRoutes, // ListenableとredirectルールをGoRouterに設定 refreshListenable: routerListenable, redirect: routerListenable.redirect, ); } }
ページ遷移のアニメーションを変更する
アプリ全体での指定
アプリ全体で指定する場合は、PageTransitionsTheme
を使って、
theme
で指定する感じ。
class PageTransitionsThemeApp extends StatelessWidget { const PageTransitionsThemeApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( pageTransitionsTheme: const PageTransitionsTheme( // デフォルトのTransition // 指定していない場合は、ZoomPageTransitionsBuilderを利用 builders: <TargetPlatform, PageTransitionsBuilder>{ TargetPlatform.android: ZoomPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), }, ), ), home: const HomePage(), ); } }
PageTransitionsTheme
のデフォルト値などは、
ソースのこのあたり。
const PageTransitionsTheme({ Map<TargetPlatform, PageTransitionsBuilder> builders = _defaultBuilders }) : _builders = builders; // デフォルトの値 static const Map<TargetPlatform, PageTransitionsBuilder> _defaultBuilders = <TargetPlatform, PageTransitionsBuilder>{ TargetPlatform.android: ZoomPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), }; // buildersに存在しないものは、ZoomPageTransitionsBuilderを利用 final PageTransitionsBuilder matchingBuilder = builders[platform] ?? const ZoomPageTransitionsBuilder();
特定ルートのみの指定
ある特定のルートに対しでのみ変更する場合は、
go_router側で設定する。
// 使いたいTransion CustomTransitionPage _pageTransition(Widget page, GoRouterState state) { return CustomTransitionPage( key: state.pageKey, child: page, transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeTransition( opacity: CurveTween(curve: Curves.easeInOutCirc).animate(animation), child: child, ); }, ); } // GoRouteでの指定 final route = GoRoute( path: '/', // builder()のかわりに、pageBuilderを利用 pageBuilder: (context, state) => _pageTransition(const HomePage(), state), ); // @TypedGoRouteでの指定 @TypedGoRoute<SplashPageRoute>(path = AppRoutes.splashPage) class HomePageRoute extends GoRouteData { @override Page buildPage(BuildContext context, GoRouterState state) { return _pageTransition(const SplashPage(), state); } }
go_router側のサンプルやFlutter公式のアニメーションに関するドキュメントはこのあたり。
いくつかライブラリも公開されている。
- 公式サンプル: transition_animations.dart · flutter/packages · GitHub
- Flutterのアニメーションに関して: Introduction to animations | Flutter
- アニメーションライブラリ(animations): Flutter 用のマテリアル モーションで美しい移行を作成する
- ページトランジションのライブラリ: page_transition | Flutter Package
- [Flutter] Navigatorを利用した画面遷移時の標準トランジション (エフェクト) を変更する方法 - Qiita
以上!! go_routerもbuilderができてだいぶ進化してる。。(*´ω`*)