はじめに🔰
Navigator2.0に対応したルーティングパッケージでroutermaster を使用していたのですが、
go_router が公式パッケージになったこともあり、乗り換えてみました。
Firebase + BLoC + go_router での認証機能 および それに伴うルーティングの一例として、
0から動くところまでを書いていきます。
開発環境
- Windows
使用パッケージ
- flutter_bloc : 8.0.1
- go_router : 3.1.0
実装
Flutterアプリの作成
任意のフォルダでflutter create
コマンドを叩いてFlutterアプリを作成します。
$ flutter create sample_auth
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
配下に設定ファイルが作成されます。
パッケージを追加していないので、現時点ではエラーになっていて問題ありません。
認証プロバイダの設定
続いてFirebaseのコンソール画面から認証プロバイダを設定していきます。
Firebaseのコンソール画面を開き、作成したプロジェクトを選択します。
左メニューの [構築] - [Authentication] を選択します。
「始める」を選択するとログインプロバイダの選択に移ります。
今回は簡単に実装が可能な匿名を選択して、有効にします。
Flutterアプリの実装
ようやくFlutter側の実装になります。
この後いくつかのファイルに分けて実装を行っていきますが、フォルダ構成は以下のように作っています。
Firebase 初期処理
まずは今回使用するパッケージを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.dart
でrunApp()
の前にFirebaseの初期処理を実装します。
void main() async {
// Firebase Initialize
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const App());
}
エラーになってしまうので、app.dart
の中身を仮実装します。
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
// TODO: 後から実装
return MaterialApp();
}
}
認証機能の実装
Firebase + BLoCで認証を実装していきます。
まずは認証に関連する 「イベント」「状態」を定義していきます。
- イベント
abstract class AuthEvent extends Equatable {
const AuthEvent();
@override
List<Object> get props => [];
}
/// アプリ起動
class AppStarted extends AuthEvent {}
/// 認証情報の変更検知
class AuthenticationUserChanged extends AuthEvent {}
- 認証状態
abstract class AuthState extends Equatable {
const AuthState();
@override
List<Object> get props => [];
}
/// 未初期化
class UnInitialized extends AuthState {}
/// 未認証
class NotAuth extends AuthState {}
/// 認証済
class Authenticated extends AuthState {}
- イベント時の処理 (仮定義)
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
のインスタンスを定義します。
final FirebaseAuth _auth;
AuthBloc(FirebaseAuth? auth)
: _auth = auth ?? FirebaseAuth.instance,
// 初期状態は「初期化されていない状態」
super(UnInitialized())
イベント別の処理実装
イベント毎に処理を実装していきます。
アプリ起動時 (AppStarted)
ユーザーの認証状態の変更を監視します。
変更を検知した際はAuthenticationoUserChanged
イベントを発火させ、取得した認証情報を渡します。
_appStarted(AppStarted event, Emitter<AuthState> emit) async {
// 認証状態変更の監視
_repository.user.listen((user) => add(AuthenticationUserChanged(user)));
}
あわせて、変更検知イベント側にも認証情報のプロパティを追加します。
const AuthenticationUserChanged(this.user);
@override
List<Object> get props => [user ?? ""];
final User? user;
認証情報の変更検知時 (AuthenticationUserChanged)
認証情報を基に 「未認証」か「認証済」 の状態を判定します。
_onAuthenticationUserChanged(
AuthenticationUserChanged event, Emitter<AuthState> emit) async {
final user = event.user;
if (user == null) {
emit(NotAuth());
} else {
emit(Authenticated());
}
}
これで認証状態を管理するBLoCが完成しました。
アプリへの組み込み
作成した認証機能を組み込みます。
アプリ起動時と同時に検知するため、app.dart
の先頭でBLoCを定義します。
@override
Widget build(BuildContext context) {
return BlocProvider<AuthBloc>(
create: (_) => AuthBloc()..add(AppStarted()),
child: Builder(
// TODO 後から実装
builder: (context) => MaterialApp(),
),
);
}
サインインやサインアウトの処理は認証状態とは直接関係がないので、
各画面の製造時に別BLoCとして追加します。
ルーティングの実装
今回は以下の3種類の画面で作成します。
- スプラッシュ画面:起動中に表示
- 認証画面:未認証状態でアプリを起動した際に表示
- ホーム画面:認証済状態でアプリを起動した際に表示
画面の実装
まずはそれぞれのページへのパスやタイトルを管理しやすいようにenum
で定義します
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をご参照下さい。
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("匿名サインイン")),
),
),
);
}
}
他画面についても同様に作成して下さい。(割愛)
静的なルーティング定義
まずはシンプルな各画面のルーティング定義を行います。
それぞれ遷移用のパスとページを定義するだけです。
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
に認証状態を持たせます。
final AuthBloc _authBloc;
AppRouter(AuthBloc bloc) : _authBloc = bloc;
認証状態の変更をGoRouter側でも検知出来るように、refreshListenable
にStreamを渡します。
GoRouter(
refreshListenable: GoRouterRefreshStream(_authBloc.stream),
...
)
認証状態に応じた画面のリダイレクト処理をGoRouter
上に定義していきます。
GoRouter(
...
redirect: (state) {
// 認証ステータスを判定
final authState = _authBloc.state;
if (authState is UnInitialized) {
// TODO 初期化されていない状態のリダイレクト処理
}
if (authState is NotAuth) {
// TODO 未認証の際のリダイレクト処理
}
if (authState is Authenticated) {
// TODO 認証済の際のリダイレクト処理
}
return null;
)
後は遷移不可能なケース毎に記載していくだけです。
今回のケースでは下記に沿ってリダイレクトを定義していきます。
- スプラッシュ画面:起動中に表示
- 認証画面:未認証状態でアプリを起動した際に表示
- ホーム画面:認証済状態でアプリを起動した際に表示
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
の定義箇所に今回定義したルーティングを指定します。
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
さいごに
他にいいやり方があったり、アンチパターンを使用している場合はご指摘頂きたいです!
乗り換えてあまり経っていませんが、go_router
は非常に使い勝手がいい印象です。
まだNavigationBar
を使った遷移周りで課題は残っているそうですが、
公式パッケージになったこともあり今後のサポートに期待です。
状態管理にProvider
やriverpod
を利用した場合のルーティングに関しては、
参考サイトに載せていますので、ぜひ参考にして下さい。
※ 当記事で作成した全ソースはGithubに公開しています。