🛣️

【Flutter】auto_routeを試す

2022/09/30に公開

auto_route

go_routerと並んでよく目にするauto_routeを今回は試してみたいと思います。

auto_router

  • Flutterのナビゲーションパッケージ
  • 強い型付けの引数を渡すことができる
  • 楽にディープリンクができる
  • コード生成を使ってルート設定を簡素化する

動作環境

動作確認は全てFlutter WebでビルドしてChrome上で確認してます。

- macOS Monterey バージョン12.1 Apple M1
- Flutter SDK バージョン 3.3.1
- auto_route バージョン 5.0.1
- Google Chrome バージョン 105.0.5195.125 (arm64)

パッケージインストール

pubspec.yaml
dependencies:              
  auto_route: ^5.0.1

dev_dependencies:              
  auto_route_generator: ^5.0.1
  build_runner:

まずはシンプルなサンプルを作成

遷移用の画面作成

  • HomePage
    • アプリ起動時のデフォルトページ
  • UserPage
home_page.dart
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'HomePage',
        ),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: ElevatedButton(
          child: const Text('UserPageへ'),
          onPressed: () => context.router.pushNamed("/user-page"),
        ),
      ),
    );
  }
}
user_page.dart
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';

class UserPage extends StatelessWidget {
  const UserPage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'UserPage',
        ),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: ElevatedButton(
          style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
          child: const Text('HomePageへ'),
          onPressed: () => context.router.pop(),
        ),
      ),
    );
  }
}

router.dart の作成

import 'package:auto_route/auto_route.dart';
import 'package:flutter_web_example/pages/home_page.dart';
import 'package:flutter_web_example/pages/user_page.dart';

(
  replaceInRouteName: 'Page,Route',
  routes: <AutoRoute>[
    AutoRoute(page: HomePage, initial: true),
    AutoRoute(page: UserPage),
  ],
)
class $AppRouter {}

ここまで作成したら、以下で build_runner build を実行します。

$ fvm flutter pub pub run build_runner build --delete-conflicting-outputs

すると以下の様な router.gr.dart が作成されます。

作成された router.gr.dart
// **************************************************************************
// AutoRouteGenerator
// **************************************************************************

// GENERATED CODE - DO NOT MODIFY BY HAND

// **************************************************************************
// AutoRouteGenerator
// **************************************************************************
//
// ignore_for_file: type=lint

// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:auto_route/auto_route.dart' as _i3;
import 'package:flutter/material.dart' as _i4;
import 'package:flutter_web_example/pages/home_page.dart' as _i1;
import 'package:flutter_web_example/pages/user_page.dart' as _i2;

class AppRouter extends _i3.RootStackRouter {
  AppRouter([_i4.GlobalKey<_i4.NavigatorState>? navigatorKey])
      : super(navigatorKey);

  
  final Map<String, _i3.PageFactory> pagesMap = {
    HomeRoute.name: (routeData) {
      return _i3.AdaptivePage<dynamic>(
        routeData: routeData,
        child: const _i1.HomePage(),
      );
    },
    UserRoute.name: (routeData) {
      return _i3.AdaptivePage<dynamic>(
        routeData: routeData,
        child: const _i2.UserPage(),
      );
    },
  };

  
  List<_i3.RouteConfig> get routes => [
        _i3.RouteConfig(
          HomeRoute.name,
          path: '/',
        ),
        _i3.RouteConfig(
          UserRoute.name,
          path: '/user-page',
        ),
      ];
}

/// generated route for
/// [_i1.HomePage]
class HomeRoute extends _i3.PageRouteInfo<void> {
  const HomeRoute()
      : super(
          HomeRoute.name,
          path: '/',
        );

  static const String name = 'HomeRoute';
}

/// generated route for
/// [_i2.UserPage]
class UserRoute extends _i3.PageRouteInfo<void> {
  const UserRoute()
      : super(
          UserRoute.name,
          path: '/user-page',
        );

  static const String name = 'UserRoute';
}

次に main.dart を以下に修正します。

import 'package:flutter/material.dart';
import 'package:flutter_web_example/router/router.gr.dart';

void main() {
  runApp(const MyApp());
}

final _appRouter = AppRouter();

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Flutter Web Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      routerDelegate: _appRouter.delegate(),
      routeInformationParser: _appRouter.defaultRouteParser(),
    );
  }
}

ここまでで実行すると以下の様に画面遷移します。

型付けの引数を渡す

user_page.dart に名前を引数で渡せる様に修正します。

class UserPage extends StatelessWidget {
  const UserPage({super.key, required this.name}); // コンストラクタの引数追加

  final String name; // 追加

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'UserPage $name', // 引数のnameを表示
        ),
        automaticallyImplyLeading: false,
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: ElevatedButton(
          style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
          child: const Text('HomePageへ'),
          onPressed: () => context.router.pushNamed("/"),
        ),
      ),
    );
  }
}

この状態でもう一度 build します。

$ fvm flutter pub pub run build_runner build --delete-conflicting-outputs

この時の router.gr.dart の差分としては以下になります。

--- a/lib/router/router.gr.dart
+++ b/lib/router/router.gr.dart
@@ -29,9 +29,13 @@ class AppRouter extends _i3.RootStackRouter {
       );
     },
     UserRoute.name: (routeData) {
+      final args = routeData.argsAs<UserRouteArgs>();
       return _i3.AdaptivePage<dynamic>(
         routeData: routeData,
-        child: const _i2.UserPage(),
+        child: _i2.UserPage(
+          key: args.key,
+          name: args.name,
+        ),
       );
     },
   };
@@ -63,12 +67,34 @@ class HomeRoute extends _i3.PageRouteInfo<void> {
 
 /// generated route for
 /// [_i2.UserPage]
-class UserRoute extends _i3.PageRouteInfo<void> {
-  const UserRoute()
-      : super(
+class UserRoute extends _i3.PageRouteInfo<UserRouteArgs> {
+  UserRoute({
+    _i4.Key? key,
+    required String name,
+  }) : super(
           UserRoute.name,
           path: '/user-page',
+          args: UserRouteArgs(
+            key: key,
+            name: name,
+          ),
         );
 
   static const String name = 'UserRoute';
 }
+
+class UserRouteArgs {
+  const UserRouteArgs({
+    this.key,
+    required this.name,
+  });
+
+  final _i4.Key? key;
+
+  final String name;
+
+  @override
+  String toString() {
+    return 'UserRouteArgs{key: $key, name: $name}';
+  }
+}

UserRouteArgs というclassが追加されており、UserRoute でargsにセットする様に変更されています。

user_page.dart へ遷移させる部分を以下の様に修正します。

context.router.push(UserRoute(name: 'taro')),

↓動作させてみると引数が渡っている事が確認できます。

この時 pushName などで必須のパラメータを渡さずに画面遷移しようとすると以下のエラーが発生します。

Uncaught (in promise) Error: UserRouteArgs can not be null because it has a required parameter

Tab Navigation

次に BottomNavigationBar の選択状態がルートと連動する様なサンプルを試してみます。
実際のサンプルイメージとしては↓の様な挙動になります。

Nested Navigation を作成する

tab_page.dart の中にネストした tab1_page.darttab2_page.darttab3_page.dart の3つが存在する様な構成で作成します。router.dart に以下を追加します。

    AutoRoute(
      path: '/tab',
      page: TabPage,
      children: [
        AutoRoute(path: 'tab1', page: Tab1Page, initial: true),
        AutoRoute(path: 'tab2', page: Tab2Page),
        AutoRoute(path: 'tab3', page: Tab3Page),
      ],
    ),

AutoTabsRouterを使ったページ作成

以下内容で tab_page.dart を作成します。

import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_web_example/router/router.gr.dart';

class TabPage extends StatelessWidget {
  const TabPage({super.key});

  
  Widget build(BuildContext context) {
    return AutoTabsRouter(
      routes: const [Tab1Route(), Tab2Route(), Tab3Route()],
      builder: (context, child, animation) {
        final tabsRouter = AutoTabsRouter.of(context);
        return Scaffold(
            body: FadeTransition(
              opacity: animation,
              child: child,
            ),
            bottomNavigationBar: BottomNavigationBar(
              currentIndex: tabsRouter.activeIndex,
              onTap: (index) {
                tabsRouter.setActiveIndex(index);
              },
              items: const [
                BottomNavigationBarItem(label: 'Tab1', icon: Icon(Icons.check)),
                BottomNavigationBarItem(label: 'Tab2', icon: Icon(Icons.check)),
                BottomNavigationBarItem(label: 'Tab3', icon: Icon(Icons.check)),
              ],
            ));
      },
    );
  }
}

各タブで表示するページも作成します。

tab1_page.dart
import 'package:flutter/material.dart';

class Tab1Page extends StatelessWidget {
  const Tab1Page({super.key});

  
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(8.0),
      child: const Text('Tab1'),
    );
  }
}

↑の要領で、 tab2_page.darttab3_page.dart も作成しときます。
次に build_runner build して router.gr.dart を更新します。

$ fvm flutter pub pub run build_runner build --delete-conflicting-outputs
更新された router.gr.dart
--- a/lib/router/router.gr.dart
+++ b/lib/router/router.gr.dart
@@ -11,26 +11,30 @@
 // ignore_for_file: type=lint
 
 // ignore_for_file: no_leading_underscores_for_library_prefixes
-import 'package:auto_route/auto_route.dart' as _i3;
-import 'package:flutter/material.dart' as _i4;
+import 'package:auto_route/auto_route.dart' as _i7;
+import 'package:flutter/material.dart' as _i8;
 import 'package:flutter_web_example/pages/home_page.dart' as _i1;
+import 'package:flutter_web_example/pages/tab_page.dart' as _i3;
+import 'package:flutter_web_example/pages/tabs/tab1_page.dart' as _i4;
+import 'package:flutter_web_example/pages/tabs/tab2_page.dart' as _i5;
+import 'package:flutter_web_example/pages/tabs/tab3_page.dart' as _i6;
 import 'package:flutter_web_example/pages/user_page.dart' as _i2;
 
-class AppRouter extends _i3.RootStackRouter {
-  AppRouter([_i4.GlobalKey<_i4.NavigatorState>? navigatorKey])
+class AppRouter extends _i7.RootStackRouter {
+  AppRouter([_i8.GlobalKey<_i8.NavigatorState>? navigatorKey])
       : super(navigatorKey);
 
   @override
-  final Map<String, _i3.PageFactory> pagesMap = {
+  final Map<String, _i7.PageFactory> pagesMap = {
     HomeRoute.name: (routeData) {
-      return _i3.AdaptivePage<dynamic>(
+      return _i7.AdaptivePage<dynamic>(
         routeData: routeData,
         child: const _i1.HomePage(),
       );
     },
     UserRoute.name: (routeData) {
       final args = routeData.argsAs<UserRouteArgs>();
-      return _i3.AdaptivePage<dynamic>(
+      return _i7.AdaptivePage<dynamic>(
         routeData: routeData,
         child: _i2.UserPage(
           key: args.key,
@@ -38,24 +42,76 @@ class AppRouter extends _i3.RootStackRouter {
         ),
       );
     },
+    TabRoute.name: (routeData) {
+      return _i7.AdaptivePage<dynamic>(
+        routeData: routeData,
+        child: const _i3.TabPage(),
+      );
+    },
+    Tab1Route.name: (routeData) {
+      return _i7.AdaptivePage<dynamic>(
+        routeData: routeData,
+        child: const _i4.Tab1Page(),
+      );
+    },
+    Tab2Route.name: (routeData) {
+      return _i7.AdaptivePage<dynamic>(
+        routeData: routeData,
+        child: const _i5.Tab2Page(),
+      );
+    },
+    Tab3Route.name: (routeData) {
+      return _i7.AdaptivePage<dynamic>(
+        routeData: routeData,
+        child: const _i6.Tab3Page(),
+      );
+    },
   };
 
   @override
-  List<_i3.RouteConfig> get routes => [
-        _i3.RouteConfig(
+  List<_i7.RouteConfig> get routes => [
+        _i7.RouteConfig(
           HomeRoute.name,
           path: '/',
         ),
-        _i3.RouteConfig(
+        _i7.RouteConfig(
           UserRoute.name,
           path: '/user-page',
         ),
+        _i7.RouteConfig(
+          TabRoute.name,
+          path: '/tab',
+          children: [
+            _i7.RouteConfig(
+              '#redirect',
+              path: '',
+              parent: TabRoute.name,
+              redirectTo: 'tab1',
+              fullMatch: true,
+            ),
+            _i7.RouteConfig(
+              Tab1Route.name,
+              path: 'tab1',
+              parent: TabRoute.name,
+            ),
+            _i7.RouteConfig(
+              Tab2Route.name,
+              path: 'tab2',
+              parent: TabRoute.name,
+            ),
+            _i7.RouteConfig(
+              Tab3Route.name,
+              path: 'tab3',
+              parent: TabRoute.name,
+            ),
+          ],
+        ),
       ];
 }
 
 /// generated route for
 /// [_i1.HomePage]
-class HomeRoute extends _i3.PageRouteInfo<void> {
+class HomeRoute extends _i7.PageRouteInfo<void> {
   const HomeRoute()
       : super(
           HomeRoute.name,
@@ -67,9 +123,9 @@ class HomeRoute extends _i3.PageRouteInfo<void> {
 
 /// generated route for
 /// [_i2.UserPage]
-class UserRoute extends _i3.PageRouteInfo<UserRouteArgs> {
+class UserRoute extends _i7.PageRouteInfo<UserRouteArgs> {
   UserRoute({
-    _i4.Key? key,
+    _i8.Key? key,
     required String name,
   }) : super(
           UserRoute.name,
@@ -89,7 +145,7 @@ class UserRouteArgs {
     required this.name,
   });
 
-  final _i4.Key? key;
+  final _i8.Key? key;
 
   final String name;
 
@@ -98,3 +154,52 @@ class UserRouteArgs {
     return 'UserRouteArgs{key: $key, name: $name}';
   }
 }
+
+/// generated route for
+/// [_i3.TabPage]
+class TabRoute extends _i7.PageRouteInfo<void> {
+  const TabRoute({List<_i7.PageRouteInfo>? children})
+      : super(
+          TabRoute.name,
+          path: '/tab',
+          initialChildren: children,
+        );
+
+  static const String name = 'TabRoute';
+}
+
+/// generated route for
+/// [_i4.Tab1Page]
+class Tab1Route extends _i7.PageRouteInfo<void> {
+  const Tab1Route()
+      : super(
+          Tab1Route.name,
+          path: 'tab1',
+        );
+
+  static const String name = 'Tab1Route';
+}
+
+/// generated route for
+/// [_i5.Tab2Page]
+class Tab2Route extends _i7.PageRouteInfo<void> {
+  const Tab2Route()
+      : super(
+          Tab2Route.name,
+          path: 'tab2',
+        );
+
+  static const String name = 'Tab2Route';
+}
+
+/// generated route for
+/// [_i6.Tab3Page]
+class Tab3Route extends _i7.PageRouteInfo<void> {
+  const Tab3Route()
+      : super(
+          Tab3Route.name,
+          path: 'tab3',
+        );
+
+  static const String name = 'Tab3Route';
+}

この状態で実行すると↑の様な挙動になるかと思います。

値を返す

値を返す方法としては以下の2通りあります。

  1. pop completer を使う

    // LoginRoute内でpopする際に値を渡す
    router.pop(true);  
    
    //  呼び出し側で結果を受け取る
    var result = await router.push(LoginRoute());
    

    ↑だとdynamic型になってしまうので、型定義をする場合は以下の様にする

    // route定義
    AutoRoute<bool>(page: LoginPage), 
    
    // LoginRoute内
    router.pop<bool>(true); 
    // 呼び出し側
    var result = await router.push<bool>(LoginRoute()); 
    
  2. 画面遷移先にcallbackを渡す

// 画面遷移先
// onRateBook をcallbackとして渡してもらう
class BookDetailsPage extends StatelessWidget {
 const BookDetailsRoute({this.book, required this.onRateBook});

  final Book book;
  final void Function(int) onRateBook;
  ...
  // popする際にcallbackを呼び出す
  onRateBook(RESULT);
  context.router.pop();

// 呼び出し時
context.router.push(
      BookDetailsRoute(
          book: book,
          onRateBook: (rating) {
           // handle result
          }),
    );

Path ParametersとQuery Parameters

ちなみに AutoRoutepath を指定していない場合 page の名前を元にpathが設定されます。

// 例
AutoRoute(page: HomePage, initial: true), // => initial: trueなのでパスは / になる
AutoRoute(page: UserPage), // page名から /user-page になる
AutoRoute(path: '/sample', page: SamplePage), // pathが指定されているので /sample になる

auto_routeでは path の設定はoptionalという扱いになっている様です。
次に、Path Parametersとして何かしら値を渡す場合は以下の様に定義します。

// 例
AutoRoute(path: '/books/:id', page: BookDetailsPage),   

↑の例だと :id に設定された値 (例 /books/9 => :id は9が設定される) がBookDetailsPageで取得する事ができます。
先ほどの HomePageUserPage を使って試してみたいと思います。

先ずは router.dart を以下に修正し、build runner build します

(
  replaceInRouteName: 'Page,Route',
  routes: <AutoRoute>[
    AutoRoute(page: HomePage, initial: true),
    AutoRoute(path: '/user/:id', page: UserPage), // ← ここを修正!!
    AutoRoute(
      path: '/tab',
      page: TabPage,
      children: [
        AutoRoute(path: 'tab1', page: Tab1Page, initial: true),
        AutoRoute(path: 'tab2', page: Tab2Page),
        AutoRoute(path: 'tab3', page: Tab3Page),
      ],
    ),
  ],
)
class $AppRouter {}

home_page.dartuser_page.dart をそれぞれ以下に修正します。

home_page.dart
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'HomePage',
        ),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column( // ↓ここから下修正 !!
          children: [
            ElevatedButton(
              child: const Text('UserPage1へ'),
              onPressed: () => context.router.pushNamed('/user/1'),
            ),
            const SizedBox(
              height: 8,
            ),
            ElevatedButton(
              child: const Text('UserPage2へ'),
              onPressed: () => context.router.pushNamed('/user/2?name=jiro'),
            ),
          ],
        ),
      ),
    );
  }
}
user_page.dart
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';

class UserPage extends StatelessWidget {
  const UserPage({
    super.key,
    ('id') this.id = -1, // ← 追加!!
     this.name = '-',  // ← 追加!! @QueryParamではなく@queryParam
  });

  final int id; // ← 追加!!
  final String name;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'UserPage $id / $name',  // ← 修正!!
        ),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: ElevatedButton(
          style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
          child: const Text('HomePageへ'),
          onPressed: () => context.router.pop(),
        ),
      ),
    );
  }
}

この状態でビルドして動かしてみると以下の様な挙動になるかと思います。

URLの :id で指定した部分が渡っているのが確認できるかと思います。
また、@queryParam というのも指定しており、名前からして想像できるようにクエリパラメータを扱うものになります。
「UserPage2へ」の時の遷移先に /user/2?name=jiro と指定しており、nameの値が @queryParam this.name に渡ってくる様な挙動になっています。

また少し分かりづらいですが、@QueryParam ではなく @queryParam を指定するとプロパティ名をキーとして使ってくれます。@PathParam も同様です。

// 例
 this.name //=> @QueryParam('name') this.name と同じ
('id') this.id // => @pathParam this.id と同じ

Custom Route Transitions

自身で画面遷移のエフェクトを使いたい場合は CustomRoute を使用して
transitionsBuilder を設定します。

    CustomRoute(
        path: '/custom'_transition,
        page: CustomTransitionPage,
        transitionsBuilder: TransitionsBuilders.fadeIn),

参考URL

Discussion