auto_route + riverpod + FirebaseAuthentication
対象者
- riverpodを使ったことがある
- auto_routeを使ったことがある
- FirebaseAuthenticationを使ったことがある
多分中級者向けの技術記事と思います😅
知ってるの前提で書くのでわからない人はご遠慮ください。
画面遷移のパッケージのauto_route
で、Firebaseログインを実装した記事がない(−_−;)
最近、auto_route
が気になって使っていたので、使い方間違っているかもだけど記事を書いてみることにしました。
プロジェクトの説明
Flutterの新規プロジェクトとFirebaseのプロジェクトを作成しておいてください。
- 開発に必要な開発環境の準備
- packageのインストール
🔥Firebase
flutter pub add firebase_core
flutter pub add firebase_auth
📦add flutter_riverpod
flutter pub add \
flutter_riverpod \
riverpod_annotation \
dev:riverpod_generator \
dev:build_runner \
dev:custom_lint \
dev:riverpod_lint
🚪add auto_route
flutter pub add auto_route
dart pub global activate auto_route_generator
コマンドでパッケージを追加できないときは、手動で配置しましょう😅
pubsepc.yaml
name: auto_route_auth_app
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: '>=3.4.1 <4.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6
firebase_core: ^3.0.0
firebase_auth: ^5.0.0
flutter_riverpod: ^2.5.1
riverpod_annotation: ^2.3.5
auto_route: ^8.1.3
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^3.0.0
auto_route_generator: ^8.0.0
riverpod_generator: ^2.4.0
build_runner: ^2.4.11
custom_lint: ^0.6.4
riverpod_lint: ^2.3.10
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
今回は動くものを作るだけなので、設計は無視です。アレンジしてみてください。
FirebaseAuthProviderを作成
- FirebaseAuthをインスタンス化したプロバイダーを作成
import 'package:firebase_auth/firebase_auth.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'auth.g.dart';
// flutter pub run build_runner watch --delete-conflicting-outputs
// FirebaseAuthのプロバイダー
FirebaseAuth firebaseAuth(FirebaseAuthRef ref) {
return FirebaseAuth.instance;
}
- StreamNotifierを使用して、認証状態の監視と、
ref.listen
で認証が通っていれば画面遷移するロジックを実行するのに使う。
import 'package:auto_route_auth_app/provider/auth.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'auth_notifier.g.dart';
/// [認証状態を監視するプロバイダー]
class StreamAuthNotifier extends _$StreamAuthNotifier {
Stream<User?> build() async* {
yield* authStateChangeFunction();
}
Stream<User?> authStateChangeFunction() async* {
yield* ref.read(firebaseAuthProvider).authStateChanges();
}
}
ログインページ・新規追加のページ・ログイン後のページ
ログイン後のページを作成します。このページですけど、サインアウトした後に、手動で画面遷移させないとログアウトとしなかったです?
go_router
みたいにリダイレクトするのを自動でやってくれないみたい?
ログアウトしたか監視してくれないようだ。authStateChanges
を使う必要があることを以前勉強会で聞いたことがあったような....
ログイン後のページ
import 'package:auto_route/auto_route.dart';
import 'package:auto_route_auth_app/presentation/router.gr.dart';
import 'package:auto_route_auth_app/provider/auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
()
class WelcomePage extends ConsumerWidget {
const WelcomePage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false, // AppBarの戻るボタンを非表示にする
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () async {
await ref.read(firebaseAuthProvider).signOut();
/// [ログアウト後の処理]
/// signOutしても画面遷移はしないので手動で行う必要がある。
if (context.mounted) {
context.router.replace(const SignInRoute());
}
},
),
],
title: const Text('Welcome to Riverpod!'),
),
body: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Welcome to Riverpod!'),
],
),
),
);
}
}
新規登録ページ
ログインと新規登録のページをConsumerStatefulWidget
にしているのは、ConsumerWidget
で、TextEditingController
を使ったときに、キーボードを閉じると入力したテキストがリセットされて消えてしまう現象が起きてしまうようで、状態を保持するために使わざるを得ませんでした💦
普段わたしが使っているflutter_hooks
を使えば、状態を保持してくれるようで、Statelessな感じにはできそう。でもStatefulWidget
を嫌う人いるんですよね😅
私も好きではないです。しかしUI側で状態管理するとなると、必要な場面が出てきますね。
なので、build
メソッドの中には、TextEditingController
は書かないほうが良い。
import 'package:auto_route/auto_route.dart';
import 'package:auto_route_auth_app/presentation/router.gr.dart';
import 'package:auto_route_auth_app/provider/auth.dart';
import 'package:auto_route_auth_app/provider/auth_notifier.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
()
class SignUpPage extends ConsumerStatefulWidget {
const SignUpPage({super.key});
ConsumerState createState() => _SignUpPageState();
}
class _SignUpPageState extends ConsumerState<SignUpPage> {
final email = TextEditingController();
final password = TextEditingController();
void dispose() {
email.dispose();
password.dispose();
super.dispose();
}
Widget build(BuildContext context) {
ref.listen(streamAuthNotifierProvider, (previous, next) {
if (next.asData?.value != null) {
context.router.replace(const WelcomeRoute());
} else {
// ユーザーが新規作成されなかったら処理を終了する。
return;
}
});
return Scaffold(
appBar: AppBar(
title: const Text('Sign Up'),
),
body: SingleChildScrollView(
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: Column(
children: [
const SizedBox(height: 50),
TextFormField(
controller: email,
decoration: const InputDecoration(
labelText: 'Email',
),
),
const SizedBox(height: 20),
TextFormField(
obscureText: true,
controller: password,
decoration: const InputDecoration(
labelText: 'Password',
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
try {
await ref
.read(firebaseAuthProvider)
.createUserWithEmailAndPassword(
email: email.text, password: password.text);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e.toString()),
),
);
}
}
},
child: const Text('Sign Up'),
),
],
),
),
),
);
}
}
ユーザーが新規作成されたら、ref.listen
を使って、ログイン後のページへ画面遷移します。新規登録に失敗すると、何も起きません。ちなみデバッグしてて気づいたが、入力フォームをタップすると、 build
メソッドが実行されるのか、ref.listen
が実行される。ずっと実行されてないと、リダイレクトみたいなことできないですけどね😅
go_router
だと、 riverpodと組み合わせれば、リダイレクトの処理で、「いい感じ」で、「画面遷移のコード書かなくても」勝手に画面遷移してくれる。スタックだって削除されている。
auto_route
だと困ったことにページによっては、スタックが削除されていないので、AppBarにバックボタンが表示されてた😱
「なのでログイン後のページでは非表示になるように設定しました笑」
仕方がない...
これがログインのページ
go_router
だとリダイレクトの処理で、ログインしているか、いないかで、画面が切り替わるけど、auto_route
にはその機能はないようだ...
FirebaseAuthのauthStateChanges
を使う必要がある。riverpod使えば、ref
メソッドを使って、ログインしていなければ、「こいつログインしてないからはじく!」って機能を使うことができるAuthGuard
クラスにある分岐処理に、認証状態を保持しているプロバイダーを渡すことができます。
import 'package:auto_route/auto_route.dart';
import 'package:auto_route_auth_app/presentation/router.gr.dart';
import 'package:auto_route_auth_app/provider/auth.dart';
import 'package:auto_route_auth_app/provider/auth_notifier.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
()
class SignInPage extends ConsumerStatefulWidget {
const SignInPage({super.key});
ConsumerState createState() => _SignInPageState();
}
class _SignInPageState extends ConsumerState<SignInPage> {
final email = TextEditingController();
final password = TextEditingController();
void dispose() {
email.dispose();
password.dispose();
super.dispose();
}
Widget build(BuildContext context) {
ref.listen(streamAuthNotifierProvider, (previous, next) {
if (next.asData?.value != null) {
context.router.replace(const WelcomeRoute());
} else {
context.router.replace(const SignInRoute());
}
});
return Scaffold(
appBar: AppBar(
title: const Text('Sign In'),
),
body: SingleChildScrollView(
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: Column(
children: [
const SizedBox(height: 50),
TextFormField(
controller: email,
decoration: const InputDecoration(
labelText: 'Email',
),
),
const SizedBox(height: 20),
TextFormField(
obscureText: true,
controller: password,
decoration: const InputDecoration(
labelText: 'Password',
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
try {
await ref
.read(firebaseAuthProvider)
.signInWithEmailAndPassword(
email: email.text, password: password.text);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e.toString()),
),
);
}
}
},
child: const Text('Sign In'),
),
TextButton(
onPressed: () {
context.router.push(const SignUpRoute());
},
child: const Text('Sign Up')),
],
),
),
),
);
}
}
AutoRouteの設定をする
AuthGuard
クラスの設定をしましよう。このファイルで、認証がされていなければ、ログインをさせない処理を実行することができます。go_router
とは違って、常に動いているわけではない。
import 'package:auto_route/auto_route.dart';
import 'package:auto_route_auth_app/presentation/router.gr.dart';
import 'package:auto_route_auth_app/provider/auth_notifier.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'auth_guard.g.dart';
(keepAlive: true)
AuthGuard authGuard(AuthGuardRef ref) {
return AuthGuard(ref: ref);
}
class AuthGuard extends AutoRouteGuard {
Ref ref;
AuthGuard({required this.ref});
void onNavigation(NavigationResolver resolver, StackRouter router) {
// resolver.next()が呼び出されるまで、ナビゲーションは一時停止されます。
// ナビゲーションを再開/継続するにはtrueを、ナビゲーションを中止するにはfalseを返します。
// authenticatedは、FirebaseAuthの現在のユーザーがnullでない場合にtrueになります。
// Providerを使用して認証状態を取得します。
final isAuth = ref.read(streamAuthNotifierProvider).asData?.value != null;
if (isAuth) {
// ユーザーが認証された場合、続行する
resolver.next(true);
} else {
// ユーザーをログインページにリダイレクトする
// tip: resolver.redirectを使用して、リダイレクトされたルート
// リゾルバが完了したときにスタックから自動的に削除されます。
resolver.redirect(const SignInRoute());
}
}
}
build_runner
のコマンドを実行すると、ルートのパスを自動生成してくれます。go_router_builder
みたいですね。普通のgo_router
にはない機能ですね。
flutter pub run build_runner watch --delete-conflicting-outputs
List<AutoRoute> get routes なの中のコードは、自動生成された後に、自分でインポートしないといけませんので、やってください。
AutoRoute(page: SignInRoute.page, initial: true)が、最初に表示されるページ。
AutoRoute(page: WelcomeRoute.page, guards: [AuthGuard(ref: ref)])が、ログインしていないと、画面遷移できないページ。
import 'package:auto_route/auto_route.dart';
import 'package:auto_route_auth_app/presentation/auth_guard.dart';
import 'package:auto_route_auth_app/presentation/router.gr.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'router.g.dart';
(keepAlive: true)
AppRouter appRouter(AppRouterRef ref) {
return AppRouter(ref: ref);
}
// flutter pub run build_runner watch --delete-conflicting-outputs
(replaceInRouteName: 'Page,Route')
class AppRouter extends $AppRouter {
final Ref ref;
AppRouter({required this.ref});
List<AutoRoute> get routes => [
AutoRoute(page: SignInRoute.page, initial: true),
AutoRoute(page: SignUpRoute.page),
// ガードを使用して認証されたユーザーのみがアクセスできるページ
AutoRoute(page: WelcomeRoute.page, guards: [AuthGuard(ref: ref)]),
];
}
android error!
minsdkを最近は、23に設定するようだ。Androidでビルドするときにハマった😅
─ Flutter Fix ─────────────────────────────────────────────────────────────────────────────────┐
│ The plugin firebase_auth requires a higher Android SDK version. │
│ Fix this issue by adding the following to the file │
│ /Users/MY_PJ/flutter_new/auto_route_auth_app/android/app/build.gradle: │
│ android { │
│ defaultConfig { │
│ minSdkVersion 23 │
│ } │
│ } │
│
修正する。
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.auto_route_auth_app"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdk = 23
targetSdk = flutter.targetSdkVersion
versionCode = flutterVersionCode.toInteger()
versionName = flutterVersionName
}
感想
今回は、auto_route + riverpod + FirebaseAuthentication
を使用して、ログイン機能を持ったデモアプリを作ってみました。調べてもどこにも情報がないから、詰まりましたね😅
公式ドキュメントが充実してるから、情報がないとか聞いたことあるが、このパッケージを使ってる会社の人でも「使い方のってないですね💦」というやりとりを私はしたので、「書いてーねーよハゲ🧑🦲」と言いたい😇
公式サイト以前見たときに、情報が古くないかと思った😅
海外の動画やpub.dev
の解説を読んで、試行錯誤してやっと動くものを作れました😭
Discussion