0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[Flutter] Firebase + BLoC + go_router で認証 + リダイレクトを実装する

Last updated at Posted at 2022-05-30

はじめに🔰

Navigator2.0に対応したルーティングパッケージでroutermaster を使用していたのですが、
go_router が公式パッケージになったこともあり、乗り換えてみました。

Firebase + BLoC + go_router での認証機能 および それに伴うルーティングの一例として、
0から動くところまでを書いていきます。

開発環境

  • Windows

使用パッケージ

実装

Flutterアプリの作成

任意のフォルダでflutter createコマンドを叩いてFlutterアプリを作成します。

$ flutter create sample_auth

見慣れた構成のアプリが生成されます
created.png

Firebaseの認証設定

FirebaseCLI & FlutterFireCLI のインストール

認証に使用するFirebaseの設定を行います。

まずは以下のコマンドを叩いてFirebaseCLIをインストールします。

$ npm install -g firebase-tools

インストールが完了したら、以下のコマンドを叩いてFirebaseにログインします。

$ firebase login
~~~
Already logged in as example@gmail.com

上記のようにユーザー名(メールアドレス)が表示されたら認証完了

最後にFlutterFireCLIをインストールします。
任意のフォルダで以下のコマンドを実行して下さい。

$ dart pub global activate flutterfire_cli

Firebaseプロジェクトの設定

CLIのインストールが完了したので、プロジェクトの設定を行っていきます。
作成したsample_authプロジェクト配下に移動し、flutterfire configureを実行します。

$ cd sample_auth
sample_auth$ flutterfire configure

ターミナル上で以下の通り設定しながら進めていきます。

Firebaseプロジェクトの選択

既存のFirebaseプロジェクトを使用するか、新規のプロジェクトを生成するかを選択します。
今回は新規のプロジェクトを作成するため、<create a new project>を選択します。

? Select a Firebase project to configure your Flutter application with ›
  hoge-example (hoge-example)
❯ <create a new project>

プロジェクト名の入力

新規で作成するプロジェクトの名称を設定します。
※ 一意なIDが必要なため、既に存在するIDの場合エラーが発生して再実施が必要になります。

? Enter a project id for your new Firebase project (e.g. my-cool-project) › sample-auth-20220527

プラットフォーム選択

対応するプラットフォームの選択を行います。
今回はサンプルのため、全てを選択した状態のまま進めます。

? Which platforms should your configuration support (use arrow keys & space to select)? ›
✔ android
✔ ios
✔ macos
✔ web

全ての設定が完了するとlib配下に設定ファイルが作成されます。

generated.png

パッケージを追加していないので、現時点ではエラーになっていて問題ありません

認証プロバイダの設定

続いてFirebaseのコンソール画面から認証プロバイダを設定していきます。

Firebaseのコンソール画面を開き、作成したプロジェクトを選択します。

firebase_project.png

左メニューの [構築] - [Authentication] を選択します。

firebase_sidemenu.png

「始める」を選択するとログインプロバイダの選択に移ります。

今回は簡単に実装が可能な匿名を選択して、有効にします。

firebase_auth.png

Flutterアプリの実装

ようやくFlutter側の実装になります。
この後いくつかのファイルに分けて実装を行っていきますが、フォルダ構成は以下のように作っています。

folder.png

Firebase 初期処理

まずは今回使用するパッケージをpubspec.ymlに定義します。

pubspec.yml
dependencies:
  flutter:
    sdk: flutter

  # ★ここから追加-------------
  # Routing
  go_router: ^3.1.0

  # BLoC
  equatable: ^2.0.3
  bloc: ^8.0.3
  flutter_bloc: ^8.0.1

  # Firebase
  firebase_core: ^1.17.1
  firebase_auth: ^3.3.19

先ほど自動生成されたfirebase_options.dartのエラーが解消されるため、
設定ファイルを使って初期化処理を呼び出していきます。

main.dartrunApp()の前にFirebaseの初期処理を実装します。

main.dart
void main() async {
  // Firebase Initialize
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  runApp(const App());
}

エラーになってしまうので、app.dartの中身を仮実装します。

app.dart
class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    // TODO: 後から実装
    return MaterialApp();
  }
}

認証機能の実装

Firebase + BLoCで認証を実装していきます。
まずは認証に関連する 「イベント」「状態」を定義していきます。

  • イベント
auth_event.dart
abstract class AuthEvent extends Equatable {
  const AuthEvent();

  @override
  List<Object> get props => [];
}

/// アプリ起動
class AppStarted extends AuthEvent {}

/// 認証情報の変更検知
class AuthenticationUserChanged extends AuthEvent {}
  • 認証状態
auth_state.dart
abstract class AuthState extends Equatable {
  const AuthState();

  @override
  List<Object> get props => [];
}

/// 未初期化
class UnInitialized extends AuthState {}

/// 未認証
class NotAuth extends AuthState {}

/// 認証済
class Authenticated extends AuthState {}
  • イベント時の処理 (仮定義)
auth_bloc.dart
class AuthBloc extends Bloc<AuthEvent, AuthState> {
  // 初期状態は「初期化されていない状態」
  AuthBloc() : super(UnInitialized()) {
    on<AppStarted>(_appStarted);
    on<AuthenticationUserChanged>(_onAuthenticationUserChanged);
  }

  _appStarted(AppStarted event, Emitter<AuthState> emit) async {
    // TODO アプリ開始時の処理
  }

  _onAuthenticationUserChanged(
      AuthenticationUserChanged event, Emitter<AuthState> emit) async {
    // TODO 認証情報の変更検知時の処理
  }
}

Firebaseの認証機能を利用するため、FirebaseAuthのインスタンスを定義します。

auth_bloc.dart
final FirebaseAuth _auth;

AuthBloc(FirebaseAuth? auth)
    : _auth = auth ?? FirebaseAuth.instance,
      // 初期状態は「初期化されていない状態」
      super(UnInitialized())

イベント別の処理実装

イベント毎に処理を実装していきます。

アプリ起動時 (AppStarted)

ユーザーの認証状態の変更を監視します。
変更を検知した際はAuthenticationoUserChangedイベントを発火させ、取得した認証情報を渡します。

auth_bloc.dart
  _appStarted(AppStarted event, Emitter<AuthState> emit) async {
    // 認証状態変更の監視
    _repository.user.listen((user) => add(AuthenticationUserChanged(user)));
  }

あわせて、変更検知イベント側にも認証情報のプロパティを追加します。

auth_event.dart
  const AuthenticationUserChanged(this.user);

  @override
  List<Object> get props => [user ?? ""];

  final User? user;
認証情報の変更検知時 (AuthenticationUserChanged)

認証情報を基に 「未認証」か「認証済」 の状態を判定します。

auth_bloc.dart
  _onAuthenticationUserChanged(
      AuthenticationUserChanged event, Emitter<AuthState> emit) async {
    final user = event.user;
    if (user == null) {
      emit(NotAuth());
    } else {
      emit(Authenticated());
    }
  }

これで認証状態を管理するBLoCが完成しました。

アプリへの組み込み

作成した認証機能を組み込みます。

アプリ起動時と同時に検知するため、app.dartの先頭でBLoCを定義します。

app.dart
  @override
  Widget build(BuildContext context) {
    return BlocProvider<AuthBloc>(
      create: (_) => AuthBloc()..add(AppStarted()),
      child: Builder(
        // TODO 後から実装
        builder: (context) => MaterialApp(),
      ),
    );
  }

サインインやサインアウトの処理は認証状態とは直接関係がないので、
各画面の製造時に別BLoCとして追加します。

ルーティングの実装

今回は以下の3種類の画面で作成します。

  • スプラッシュ画面:起動中に表示
  • 認証画面:未認証状態でアプリを起動した際に表示
  • ホーム画面:認証済状態でアプリを起動した際に表示

画面の実装

まずはそれぞれのページへのパスやタイトルを管理しやすいようにenumで定義します

app_pages.dart
enum AppPages {
  signin,
  home,
  splash,
}

extension AppPageExtension on AppPages {
  String get toPath {
    switch (this) {
      case AppPages.signin:
        return "/signin";
      case AppPages.home:
        return "/";
      case AppPages.splash:
        return "/splash";
      default:
        return "/";
    }
  }

  String get toTitle {
    switch (this) {
      case AppPages.signin:
        return "Sign In";
      case AppPages.home:
        return "Home";
      case AppPages.splash:
        return "Splash";
      default:
        return "Home";
    }
  }
}

定義した各画面を実装します。

認証画面では匿名ログインボタンを用意します。

SignInBlocについては、認証周りの実装とは関係がないので割愛します。
詳細はGithubをご参照下さい。

sing_in_page.dart
class SignInPage extends StatelessWidget {
  const SignInPage({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocListener<SignInBloc, SignInState>(
      listener: (context, state) {
        if (state is SignInStateProgress) {
          ProgressDialog.show(context);
        } else {
          ProgressDialog.hide(context);
        }
      },
      child: Scaffold(
        appBar: AppBar(title: Text(AppPages.signin.toTitle)),
        body: Center(
          child: ElevatedButton(
              // 認証イベントを呼び出す
              onPressed: () => BlocProvider.of<SignInBloc>(context)
                  .add(SignInWithAnonymous()),
              child: const Text("匿名サインイン")),
        ),
      ),
    );
  }
}

他画面についても同様に作成して下さい。(割愛)

静的なルーティング定義

まずはシンプルな各画面のルーティング定義を行います。
それぞれ遷移用のパスとページを定義するだけです。

app_router.dart
class AppRouter {
  GoRouter get goRouter => _goRouter;

  AppRouter();

  late final GoRouter _goRouter = GoRouter(
    initialLocation: AppPages.home.toPath,
    routes: <GoRoute>[
      GoRoute(
        path: AppPages.home.toPath,
        builder: (context, state) => const HomePage(),
      ),
      GoRoute(
        path: AppPages.signin.toPath,
        builder: (context, state) => const SignInPage(),
      ),
      GoRoute(
        path: AppPages.splash.toPath,
        builder: (context, state) => const SplashPage(),
      ),
    ],
  );
}

認証状態によるリダイレクト定義

先ほど実装したAuthBlocをルーティング処理にも組み込みます。
まずはAppRouterに認証状態を持たせます。

app_router.dart
  final AuthBloc _authBloc;
  AppRouter(AuthBloc bloc) : _authBloc = bloc;

認証状態の変更をGoRouter側でも検知出来るように、refreshListenableにStreamを渡します。

app_router.dart
GoRouter(
refreshListenable: GoRouterRefreshStream(_authBloc.stream),
...
)

認証状態に応じた画面のリダイレクト処理をGoRouter上に定義していきます。

app_router.dart
GoRouter(
...
redirect: (state) {
    // 認証ステータスを判定
    final authState = _authBloc.state;

    if (authState is UnInitialized) {
    // TODO 初期化されていない状態のリダイレクト処理
    }

    if (authState is NotAuth) {
    // TODO 未認証の際のリダイレクト処理
    }

    if (authState is Authenticated) {
    // TODO 認証済の際のリダイレクト処理
    }
    
    return null;
)

後は遷移不可能なケース毎に記載していくだけです。
今回のケースでは下記に沿ってリダイレクトを定義していきます。

  • スプラッシュ画面:起動中に表示
  • 認証画面:未認証状態でアプリを起動した際に表示
  • ホーム画面:認証済状態でアプリを起動した際に表示
app_router.dart
if (authState is UnInitialized &&
    state.location != AppPages.splash.toPath) {
  // 初期化されていない かつ スプラッシュ画面でない場合
  return AppPages.splash.toPath;
}

if (authState is NotAuth && state.location != AppPages.signin.toPath) {
  // 未認証 かつ 認証画面でない場合
  return AppPages.signin.toPath;
}

if (authState is Authenticated &&
    state.location != AppPages.home.toPath) {
  // 認証済 かつ ホーム画面でない場合
  return AppPages.home.toPath;
}

アプリへの組み込み

定義したルーティングをアプリに組み込みます。

MaterialAppの定義箇所に今回定義したルーティングを指定します。

app.dart
Builder(
    builder: (context) {
      final authBloc = BlocProvider.of<AuthBloc>(context);
      final appRouter = AppRouter(authBloc);

      return MaterialApp.router(
        routeInformationParser: appRouter.goRouter.routeInformationParser,
        routerDelegate: appRouter.goRouter.routerDelegate,
      );
    },
  )

これで認証状態に応じたルーティングが行われます。

Demo

demo.gif

さいごに

他にいいやり方があったり、アンチパターンを使用している場合はご指摘頂きたいです!

乗り換えてあまり経っていませんが、go_routerは非常に使い勝手がいい印象です。

まだNavigationBarを使った遷移周りで課題は残っているそうですが、
公式パッケージになったこともあり今後のサポートに期待です。

状態管理にProviderriverpodを利用した場合のルーティングに関しては、
参考サイトに載せていますので、ぜひ参考にして下さい。

※ 当記事で作成した全ソースはGithubに公開しています。

参考サイト

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?