🔒

auto routeのAuthGuardを使う

2023/07/24に公開

リダイレクトの処理ができるらしい?

auto routeには、go_routerのようにリダイレクトの処理があるらしい?

🎁必要なパッケージを追加しておこう

https://pub.dev/packages/auto_route
https://pub.dev/packages/auto_route_generator
https://pub.dev/packages/build_runner

✅auth routeの設定の仕方

以前書いた記事を参考にしてもらうと必要なパッケージと設定について理解できると思われます。
https://zenn.dev/joo_hashi/articles/487e3811eb0ddb

🔒ルートガード

ルートガードは、ミドルウェアやインターセプターのようなものだと考えてください。ルートは、割り当てられたガードを通さなければスタックに追加できません。ガードは、特定のルートへのアクセスを制限するのに便利です。

auto_routeパッケージのAutoRouteGuardを拡張してルートガードを作成します。
のAutoRouteGuardを拡張し、onNavigationメソッドの中にロジックを実装することでルートガードを作成します。

class AuthGuard extends AutoRouteGuard {                
                 
 void onNavigation(NavigationResolver resolver, StackRouter router) {                
 // the navigation is paused until resolver.next() is called with either                 
 // true to resume/continue navigation or false to abort navigation                
     if(authenticated){                
       // if user is authenticated we continue                
        resolver.next(true);                
      }else{                
         // we redirect the user to our login page 
         // tip: use resolver.redirect to have the redirected route
          // automatically removed from the stack when the resolver is completed
         resolver.redirect(LoginRoute(onResult: (success){                
                // if success == true the navigation will be resumed                
                // else it will be aborted                
               resolver.next(success);                
          }));                
         }                    
     }                
}

重要: resolver.next()は一度だけ呼び出すべきである。

NavigationResolverオブジェクトには、resolver.routeプロパティを呼び出すことでアクセスできる保護されたルートと、resolver.pendingRoutesを呼び出すことでアクセスできる保留中のルートのリスト(もしあれば)が含まれています。

ここで、保護したいルートにガードを割り当てます。

AutoRoute(page: ProfileRoute.page, guards: [AuthGuard()]);

AuthGuardの設定をしたコード

公式のコードに少し手を加えただけです。

auth.dart
import 'package:auto_route/auto_route.dart';
import 'package:auto_route_tutorial/routes/router.dart';
import 'package:firebase_auth/firebase_auth.dart';

class AuthGuard extends AutoRouteGuard {
  
  void onNavigation(NavigationResolver resolver, StackRouter router) {
    // resolver.next()が呼び出されるまで、ナビゲーションは一時停止されます。
    // ナビゲーションを再開/継続するにはtrueを、ナビゲーションを中止するにはfalseを返します。
    // authenticatedは、FirebaseAuthの現在のユーザーがnullでない場合にtrueになります。
    final authenticated = FirebaseAuth.instance.currentUser != null;
    // ユーザーが認証されているかどうかをチェックします。
    if (authenticated) {
      // ユーザーが認証された場合、続行する
      resolver.next(true);
    } else {
      // ユーザーをログインページにリダイレクトする
      // tip: resolver.redirectを使用して、リダイレクトされたルート
      // リゾルバが完了したときにスタックから自動的に削除されます。
      resolver.redirect(const AccountRoute());
    }
  }
}

UIを作る

ルートの指定に必要なページを作成する。ログイン画面とログイン後の画面を作りましょう。

account.dart
import 'package:auto_route/auto_route.dart';
import 'package:auto_route_tutorial/routes/router.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final emailProvider = StateProvider((ref) => TextEditingController());
final passwordProvider = StateProvider((ref) => TextEditingController());

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    final email = ref.watch(emailProvider);
    final password = ref.watch(passwordProvider);

    return Scaffold(
        appBar: AppBar(),
        body: Column(
          children: [
            TextFormField(
              controller: email,
              decoration: const InputDecoration(
                labelText: 'Email',
              ),
            ),
            TextFormField(
              controller: password,
              decoration: const InputDecoration(
                labelText: 'Password',
              ),
            ),
            ElevatedButton(
              child: const Text('Login'),
              onPressed: () async {
                await FirebaseAuth.instance.signInWithEmailAndPassword(
                  email: email.text,
                  password: password.text,
                );
                email.clear();
                password.clear();
                if (context.mounted) {
                  AutoRouter.of(context).replace(const BookRoute());
                }
              },
            ),
            const SizedBox(height: 20),
            ElevatedButton(
                onPressed: () {
                  final auth = FirebaseAuth.instance.currentUser;
                  if (auth?.uid == null) {
                    AlertDialog(
                      title: const Text('ログインしてません!'),
                      content: const Text('ログインしてから再度お試しください'),
                      actions: [
                        TextButton(
                          onPressed: () {
                            AutoRouter.of(context).pop();
                          },
                          child: const Text('OK'),
                        ),
                      ],
                    );
                  }
                  AutoRouter.of(context).replace(const BookRoute());
                },
                child: const Text('ログインせずに画面遷移')),
          ],
        ));
  }
}

ログイン後の画面

import 'package:auto_route/auto_route.dart';
import 'package:auto_route_tutorial/routes/router.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final authProvider = Provider((_) => FirebaseAuth.instance.currentUser);

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    final uuid = ref.watch(authProvider)?.uid;
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.greenAccent,
        actions: [
          IconButton(
              onPressed: () async {
                await FirebaseAuth.instance.signOut();
                if (context.mounted) {
                  // Appbarに戻るボタンがあるので、pushではなくreplaceを使う
                  AutoRouter.of(context).replace(const AccountRoute());
                }
              },
              icon: const Icon(Icons.logout))
        ],
        title: const Text('Book'),
      ),
      body: Center(child: Text(uuid ?? 'nullだったら、ユーザーはログインしてない!')),
    );
  }
}

ルートの設定をするコード

auto routeの設定に必要なコードを書く。guards: [AuthGuard()]をつけたルートは、認証が通ってないと画面遷移できない。

router.dart
import 'package:auto_route/auto_route.dart';
import 'package:auto_route_tutorial/routes/auth.dart';
import 'package:auto_route_tutorial/screen/account.dart';
import 'package:auto_route_tutorial/screen/book.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

part 'router.gr.dart';

final appRouterProvider = Provider((ref) => AppRouter());

()
class AppRouter extends _$AppRouter {
  // _をつけないと怒られる!

  
  List<AutoRoute> get routes => [
        // ここにルーティングを追加していく
        AutoRoute(page: AccountRoute.page, initial: true),
        AutoRoute(page: BookRoute.page, guards: [AuthGuard()]),
      ];
}

自動生成するコマンドを実行する

flutter pub run build_runner build

自動生成されたファイルで、作成したページをimportする。

route.gr.dart
// GENERATED CODE - DO NOT MODIFY BY HAND

// **************************************************************************
// AutoRouterGenerator
// **************************************************************************

// ignore_for_file: type=lint
// coverage:ignore-file

part of 'router.dart';

abstract class _$AppRouter extends RootStackRouter {
  // ignore: unused_element
  _$AppRouter({super.navigatorKey});

  
  final Map<String, PageFactory> pagesMap = {
    BookRoute.name: (routeData) {
      return AutoRoutePage<dynamic>(
        routeData: routeData,
        child: const BookPage(),
      );
    },
    AccountRoute.name: (routeData) {
      return AutoRoutePage<dynamic>(
        routeData: routeData,
        child: const AccountPage(),
      );
    },
  };
}

/// generated route for
/// [BookPage]
class BookRoute extends PageRouteInfo<void> {
  const BookRoute({List<PageRouteInfo>? children})
      : super(
          BookRoute.name,
          initialChildren: children,
        );

  static const String name = 'BookRoute';

  static const PageInfo<void> page = PageInfo<void>(name);
}

/// generated route for
/// [AccountPage]
class AccountRoute extends PageRouteInfo<void> {
  const AccountRoute({List<PageRouteInfo>? children})
      : super(
          AccountRoute.name,
          initialChildren: children,
        );

  static const String name = 'AccountRoute';

  static const PageInfo<void> page = PageInfo<void>(name);
}

main.dartに、プロバイダーを使用して、ルートの監視を行う設定をする。

main.dart
import 'package:auto_route_tutorial/firebase_options.dart';
import 'package:auto_route_tutorial/routes/router.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

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

class MyApp extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final _appRouter = ref.watch(appRouterProvider);// appRouterProviderは、ルーティングの設定を管理する

    return MaterialApp.router(
      debugShowCheckedModeBanner: false,
      routerDelegate: _appRouter.delegate(),// routerDelegateは、ルーティングの状態を管理する
      routeInformationParser: _appRouter.defaultRouteParser(),// routeInformationParserは、URLをルーティングに変換する
    );
  }
}

本当にリダイレクトしているのか?

FirebaseAuthを使用してロジックを作ってみた。状態管理もしたので、Riverpodも導入した。
https://pub.dev/packages/firebase_core
https://pub.dev/packages/firebase_auth
https://pub.dev/packages/flutter_riverpod

デバッグして確認してみる

AuthGuardを使うと、ログインしていないと、画面遷移できずログインをするページにリダイレクトされた!
ただ画面遷移するコードは使えない!

Authenticated: falseと表示されているので、認証は通っていない。

FirebaseAuthでログインができれば、ログイン後のページへリダイレクトする。

Authenticated: trueと表示されているので、認証は通っている。

ファイルを保存してホットリロードしたり、更新をすると認証済なので、Bookページへリダイレクトする。

最後に

auto routetでFirebaseAuthenticationを使用した、リダイレクトの処理を実装してみました。AIに頼ったりしても機能を実装できませんでしたが、ロジカルシンキングなるものを使って課題の解決に取り組みました。

やったこと

  • 原因を探る
  • 過程を考える
  • 仮説を立てる

久しぶりに、AIに頼らずに自分の感と経験で認証機能の実装ができない課題を解決しました。本当にこれでいいのか疑問ですが😅
こちらが完成品のコード
https://github.com/sakurakotubaki/AutoRoute/tree/feature/auth

Discussion