🏑

go router builderを使ってみた

2023/10/26に公開

Overview

https://pub.dev/packages/go_router_builder
どんなパッケージなのかといいますと、go routerをtype safe(型安全)に扱えるようにしたパッケージですね。

type safe

〔プログラミング言語などが〕型安全 な◆変数の型を区別し型ごとに許される操作を限定することで、ある種の誤りを検出したりメモリーのアクセス違反を防止したりできること。

こんな解説もありました。

動画も作ってみた😅
https://www.youtube.com/watch?v=OtSK7neuNIo

summary

普通のgo routerと何が違うのか?
パスとか指定して、そこに画面遷移させるコードを書くわけですけど、ルートを定義してコードを自動生成すると、画面遷移の設定されたコードを使うことができるようになります。


まずは、パッケージを追加しよう。

go router builderを使うための環境構築をする。

  1. riverpodをインストールする
flutter pub add flutter_riverpod
flutter pub add riverpod_annotation
flutter pub add dev:riverpod_generator
flutter pub add dev:build_runner
flutter pub add dev:custom_lint
flutter pub add dev:riverpod_lint
  1. go router builderをインストールする
flutter pub add go_router
flutter pub add go_router_builder

🍐UIを作る

  1. 最初に表示するページ
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../router/router.dart';

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Go Router Builder'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
                onPressed: () {
                  // AppBarの戻るボタンが表示されない画面遷移のコード
                  const NextRoute().push(context);
                },
                child: const Text('Next')),
            const SizedBox(height: 20),
            ElevatedButton(
                onPressed: () {
                  // Stackが削除されて、AppBarに戻るボタンが表示されないコード
                  const NextRoute().go(context);
                },
                child: const Text('go')),
          ],
        ),
      ),
    );
  }
}
  1. 🍇画面遷移して表示するページ
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';

import '../router/router.dart';

class NextPage extends ConsumerWidget {
  const NextPage({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: const Text('Next Page'),
      ),
      body: Center(
        child: Column(
          children: [
            ElevatedButton(
              onPressed: () {
                // pushを使った場合は前のページへ画面遷移できるコード
                context.pop();
              },
              child: const Text('back'),
            ),
          ],
        ),
      ),
    );
  }
}

🚄ルートを定義する

riverpod_generatorを使っているので、今までとは書き方が違って戸惑うことがあると思いますが、書き方が関数ぽくなっただけです。コマンドを打つとプロバイダーを定義したファイルが自動生成されます。

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../page/home_page.dart';
import '../page/next_page.dart';

part 'router.g.dart';// ファイル名と同じ名前にして、.g.dartとつける
// flutter pub run build_runner watch --delete-conflicting-outputs

GoRouter router(RouterRef ref) {
  return GoRouter(
      debugLogDiagnostics: true,
      routes: $appRoutes,// 自動生成されたファイルからパスを読み込む
      // 404ページを指定
      errorPageBuilder: (context, state) {
        return const MaterialPage(
            child: Scaffold(
              body: Center(
                child: Text('Page not found'),
              ),
            ));
      });
}

/// [この位置にルートを定義する]
/* buildメソッドの設定が変わっていた:
https://pub.dev/documentation/go_router_builder/latest/
*/

/// [HomePageのルート]
<HomeRoute>(
    path: '/',
)
class HomeRoute extends GoRouteData {
  const HomeRoute();

  
  Widget build(BuildContext context, GoRouterState state) {
    return const HomePage();
  }
}

/// [NextPageのルート]
<NextRoute>(
  path: '/next',
)
class NextRoute extends GoRouteData {
  const NextRoute();

  
  Widget build(BuildContext context, GoRouterState state) {
    return const NextPage();
  }
}

🔫ファイルを自動生成するコマンドを打つ

flutter pub run build_runner watch --delete-conflicting-outputs

こんなファイルが自動生成されれば成功です。

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'router.dart';

// **************************************************************************
// GoRouterGenerator
// **************************************************************************

List<RouteBase> get $appRoutes => [
      $homeRoute,
      $nextRoute,
    ];

RouteBase get $homeRoute => GoRouteData.$route(
      path: '/',
      factory: $HomeRouteExtension._fromState,
    );

extension $HomeRouteExtension on HomeRoute {
  static HomeRoute _fromState(GoRouterState state) => const HomeRoute();

  String get location => GoRouteData.$location(
        '/',
      );

  void go(BuildContext context) => context.go(location);

  Future<T?> push<T>(BuildContext context) => context.push<T>(location);

  void pushReplacement(BuildContext context) =>
      context.pushReplacement(location);

  void replace(BuildContext context) => context.replace(location);
}

RouteBase get $nextRoute => GoRouteData.$route(
      path: '/next',
      factory: $NextRouteExtension._fromState,
    );

extension $NextRouteExtension on NextRoute {
  static NextRoute _fromState(GoRouterState state) => const NextRoute();

  String get location => GoRouteData.$location(
        '/next',
      );

  void go(BuildContext context) => context.go(location);

  Future<T?> push<T>(BuildContext context) => context.push<T>(location);

  void pushReplacement(BuildContext context) =>
      context.pushReplacement(location);

  void replace(BuildContext context) => context.replace(location);
}

// **************************************************************************
// RiverpodGenerator
// **************************************************************************

String _$routerHash() => r'70de5cee48590e74afaedb0dfdcaa912a2148ae6';

/// See also [router].
(router)
final routerProvider = AutoDisposeProvider<GoRouter>.internal(
  router,
  name: r'routerProvider',
  debugGetCreateSourceHash:
      const bool.fromEnvironment('dart.vm.product') ? null : _$routerHash,
  dependencies: null,
  allTransitiveDependencies: null,
);

typedef RouterRef = AutoDisposeProviderRef<GoRouter>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

ルートを定義したプロバイダーを読み込んでアプリをビルドして操作してみましょう!

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

import '../router/router.dart';

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp.router(
      debugShowCheckedModeBanner: false,
      routerConfig: ref.watch(routerProvider),
      title: 'Flutter Demo',
      theme: ThemeData(
      ),
    );
  }
}

main.dartはコード分けすぎてこれだけになりました。

main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hello/page/my_app.dart';


void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // await Firebase.initializeApp(
  //   options: DefaultFirebaseOptions.currentPlatform,
  // );
  runApp(const ProviderScope(child: MyApp()));
}

これが最初の画面

これがpushを使った例: AppBarに戻るボタンが表示される

これがgoを使った例: AppBarに戻るボタンが表示されなくなる。context.popで前のページへ戻ろうとすると、画面遷移のエラーが発生する😱

thoughts

使ってみた感想ですが、go routerってバージョンアップが早いので、go router builderが新しいバージョンだとサポートされていないことがあるらしいので、もし導入する場合は注意が必要かもしれません。

Discussion