スマレジの テックファーム(SES 部門) でWebエンジニアとして働いている やまて(@r_yamate) と申します。
実務では 2023 年 3 月末まで、 SES の派遣先のテーブルオーダーシステムの機能改修の設計などを担当しました。
2023 年 4 月からは、スマレジの関連アプリの開発業務を担当しています。触ったことのなかった Flutter での開発で、日々奮闘中です。
はじめに
今回は、 Flutter アプリに Firebase プロダクトの Authentication を導入する手順についてまとめます。
以前投稿した「Flutter アプリに Firebase を導入する手順」を一つ前のステップとしている関連記事です。
目次
環境
- MacBook Pro(intel)
- macOS Monterey 12.6.5
- Flutter 3.7.1
- Dart 2.19.1
Firebase Authentication とは
Firebase Authenticationは、安全な認証システムを簡単に構築できるサービスです。
公式ドキュメントでの説明は以下のとおりです。
Firebase Authentication は、安全な認証システムの構築を容易にし、エンドユーザーのログインや初期登録のエクスペリエンスを向上させることを目的としています。メールアドレスとパスワードの組み合わせ、電話認証、Google、Twitter、Facebook、GitHub のログインなどに対応したエンドツーエンドの ID ソリューションです。
今回は手始めにユーザーがメールアドレスとパスワードを使用して Firebase での認証ができるようにします。(Google や GitHub でのログインなどにも対応できるとのことでいつかはやってみたいです)
1. Firebase コンソールでの操作
1-1. Authentication を設定する
まずは、 Firebase コンソール で、左側のメニューから「Authentication」を選択します。
「始める」ボタンをクリックします。
1-2. メール/パスワード認証を有効にする
次に、「Sign-in method」(サインイン方法)タブを選択し、「メール/パスワード」の行の編集アイコンをクリックします。
メール/パスワード認証を有効化します。
設定が完了したら、「保存」ボタンをクリックします。
これで、 Firebase Authentication の初期設定は完了です。
2. プラグイン追加とコマンドの実行
※ 以下の記事の 3. Firebase の初期化とプラグインの追加 の手順まで実施した状態である必要があります。
2-1. プラグイン firebase_auth の追加
まず、 firebase_auth プラグインをプロジェクトに追加します。このプラグインは、 Firebase Authentication の Flutter 用のライブラリです。
編集:pubspec.yaml
dependencies:
flutter:
sdk: flutter
firebase_core: ^2.15.1
+ firebase_auth: ^4.4.0
2-2. 依存関係の更新と設定
以下のコマンドを実行して依存関係を更新し、 Firebase の設定を行います。
# 依存関係を更新 flutter pub get # Firebase CLIにログインする firebase login # FlutterFireの設定を行う flutterfire configure # Firebase CLIからログアウトする firebase logout
これで、 Firebase Authentication を Flutter アプリに導入する準備が整いました。
3. メール/パスワード認証の実装
3-1. 認証状態の Stream の設定
Stream から現在の認証状態のデータを読み取り、そのデータをアプリケーションの他の部分で使用できるようにします。
編集:lib/main.dart
import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:provider/provider.dart'; import 'firebase_options.dart'; import 'routers/router.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); runApp( StreamProvider<User?>.value( initialData: null, value: FirebaseAuth.instance.authStateChanges(), child: const MyApp(), ), ); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp.router( routerConfig: goRouter, ); } }
説明
initialData: null
(初期データ)
- Stream から最初のデータが到着するまで、
User?
タイプのnull
が初期データとして設定されます。
value: FirebaseAuth.instance.authStateChanges()
(Stream の値)
- Firebase の認証状態が変更されるたびに更新される Stream です。この Stream は
User?
タイプのデータを持っており、ユーザーの認証状態が変更(ログイン、ログアウト、アカウント削除など)されると更新されます。
child: const MyApp(),
(子要素)
この設定により、アプリケーションのどこからでも、 FirebaseAuth
の現在の認証状態にアクセスすることができます。
例えば、以下のように BuildContext
から認証状態(User?
)を取得することができます。
User? user = Provider.of<User?>(context);
3-2. ルーティングと認証状態の統合
編集:lib/routers/router.dart
import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import '../screens/home_screen.dart'; import '../screens/login_screen.dart'; import '../screens/signup_screen.dart'; import '../screens/splash_screen.dart'; const String splashPath = '/'; const String homePath = '/home'; const String loginPath = '/login'; const String signupPath = '/signup'; final GoRouter goRouter = GoRouter( routes: <RouteBase>[ GoRoute( path: splashPath, builder: (BuildContext context, GoRouterState state) { return const SplashScreen(); }, ), GoRoute( path: homePath, builder: (BuildContext context, GoRouterState state) { User? user = Provider.of<User?>(context); if (user == null) { return LoginScreen(); } else { return HomeScreen(); } }, ), GoRoute( path: loginPath, builder: (BuildContext context, GoRouterState state) { return LoginScreen(); }, ), GoRoute( path: signupPath, builder: (BuildContext context, GoRouterState state) { return SignupScreen(); }, ), ], );
説明
User? user = Provider.of<User?>(context);
- ここで
Provider
を使用して、現在の認証状態(User?
)を取得しています。現在の認証状態を取得することで、ユーザーがログインしているかどうかを判断し、適切な画面に遷移させます。
3-3. 認証サービスの実装
以下の記事の各メソッドを実行するメソッドを実装します。
createUserWithEmailAndPassword
signInWithEmailAndPassword
signOut
編集:lib/services/auth_service.dart
import 'package:firebase_auth/firebase_auth.dart'; /// Firebase Authenticationを使ってユーザーの認証操作を提供するクラス。 class AuthService { final FirebaseAuth _auth; /// コンストラクタ。 /// /// FirebaseAuthのインスタンスを注入できる。デフォルトはFirebaseAuth.instance。 /// /// [firebaseAuth]はFirebaseAuthのインスタンス。 AuthService({FirebaseAuth? firebaseAuth}) : _auth = firebaseAuth ?? FirebaseAuth.instance; /// 認証状態の変更を監視するStreamを提供する。 Stream<User?> get authStateChanges => _auth.authStateChanges(); /// 新規ユーザーを作成して認証する。 /// /// [emailAddress]はメールアドレス、 /// [password]はパスワード。 /// /// エラーメッセージを返す場合がある。 Future<String?> createUserWithEmailAndPassword( String emailAddress, String password, ) async { try { await _auth.createUserWithEmailAndPassword( email: emailAddress, password: password, ); return null; } on FirebaseAuthException catch (e) { return _getErrorMessageFromCode(e.code); } } /// メールアドレスとパスワードでログインする。 /// /// [emailAddress]はメールアドレス、 /// [password]はパスワード。 /// /// エラーメッセージを返す場合がある。 Future<String?> signInWithEmailAndPassword( String emailAddress, String password, ) async { try { await _auth.signInWithEmailAndPassword( email: emailAddress, password: password, ); return null; } on FirebaseAuthException catch (e) { return _getErrorMessageFromCode(e.code); } } /// ユーザーをログアウトする。 Future<void> signOut() async { await _auth.signOut(); } /// Firebaseエラーコードをもとにエラーメッセージを生成する。 /// /// [errorCode]はFirebaseから返されるエラーコード。 /// /// 生成されたエラーメッセージを返す。 String _getErrorMessageFromCode(String errorCode) { const errorMessages = { 'invalid-email': 'メールアドレスの形式が正しくありません。', 'user-not-found': 'このメールアドレスに該当するユーザーが見つかりません。', 'wrong-password': 'パスワードが間違っています。', 'too-many-requests': '多数のログイン失敗があったため、このアカウントへのアクセスは一時的に無効化されています。' 'パスワードをリセットすることで直ちに復元できます、または後で再試行してください。', 'weak-password': 'パスワードは最低6文字以上である必要があります。', 'email-already-in-use': 'このメールアドレスは既に使用されています。', 'unknown': 'エラーが発生しました。もう一度お試しください。', }; return errorMessages[errorCode] ?? 'エラーが発生しました。もう一度お試しください。'; } }
説明
AuthService
クラス
- このクラスは Firebase Authentication の各種認証メソッドをラップしています。
createUserWithEmailAndPassword
メソッド
- 新規ユーザーを作成して認証します。
signInWithEmailAndPassword
メソッド
- メールアドレスとパスワードでログインします。
signOut
メソッド
- ユーザーをログアウトします。
_getErrorMessageFromCode
メソッド
- Firebase から返されるエラーコードを日本語のエラーメッセージに変換します。
3-4. 認証画面の実装
新規登録画面
編集:lib/screens/signup_screen.dart
import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import '../services/auth_service.dart'; import '../routers/router.dart'; /// メールアドレスの形式を確認する正規表現 final RegExp _emailRegExp = RegExp( r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', ); /// 新規登録画面。 /// /// この画面でユーザーは新しいアカウントを作成する。 class SignupScreen extends StatelessWidget { final TextEditingController _emailController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final TextEditingController _passwordConfirmController = TextEditingController(); final AuthService _authService = AuthService(); SignupScreen({Key? key}) : super(key: key); /// 新規ユーザーを作成する。 /// /// ユーザーが入力したメールアドレスとパスワードを使用してFirebaseで新しいアカウントを作成する。 Future<void> _signup( ScaffoldMessengerState scaffoldMessenger, BuildContext context, ) async { final email = _emailController.text.trim(); final password = _passwordController.text; final passwordConfirm = _passwordConfirmController.text; if (!_validateFields(email, password, passwordConfirm, scaffoldMessenger)) { return; } try { final errorMsg = await _authService.createUserWithEmailAndPassword( email, password, ); if (errorMsg == null) { goRouter.go(homePath); } else { scaffoldMessenger.showSnackBar(SnackBar(content: Text(errorMsg))); return; } } on FirebaseAuthException catch (e) { scaffoldMessenger .showSnackBar(SnackBar(content: Text(e.message ?? 'エラーが発生しました。'))); } } void _showErrorSnackBar( ScaffoldMessengerState scaffoldMessenger, String message, ) { scaffoldMessenger.showSnackBar(SnackBar(content: Text(message))); } bool _validateFields( String email, String password, String passwordConfirm, ScaffoldMessengerState scaffoldMessenger, ) { if (email.isEmpty || password.isEmpty || passwordConfirm.isEmpty) { _showErrorSnackBar(scaffoldMessenger, 'すべてのフィールドを入力してください。'); return false; } if (!_emailRegExp.hasMatch(email)) { _showErrorSnackBar(scaffoldMessenger, '有効なメールアドレスを入力してください。'); return false; } if (password.length < 6) { _showErrorSnackBar(scaffoldMessenger, 'パスワードは6文字以上でなければなりません。'); return false; } if (password != passwordConfirm) { _showErrorSnackBar(scaffoldMessenger, 'パスワードが一致しません'); return false; } return true; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('新規登録')), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ TextField( controller: _emailController, decoration: const InputDecoration(labelText: 'メールアドレス'), ), TextField( controller: _passwordController, decoration: const InputDecoration(labelText: 'パスワード'), obscureText: true, ), TextField( controller: _passwordConfirmController, decoration: const InputDecoration(labelText: 'パスワード確認'), obscureText: true, ), ElevatedButton( onPressed: () { final scaffoldMessenger = ScaffoldMessenger.of(context); _signup(scaffoldMessenger, context); }, child: const Text('新規登録'), ), TextButton( onPressed: () => goRouter.go(loginPath), child: const Text('ログイン'), ), ], ), ), ); } }
説明
SignupScreen
クラス
- 新規登録画面を表示するウィジェットです。
_signup
メソッド
- ユーザーが入力した情報を用いて新規登録を行います。成功した場合はホーム画面に遷移し、失敗した場合はエラーメッセージを表示します。
_validateFields
メソッド
- 入力フィールドのバリデーションを行います。
ログイン画面
編集:lib/screens/login_screen.dart
import 'package:flutter/material.dart'; import 'package:firebase_auth/firebase_auth.dart'; import '../routers/router.dart'; import '../services/auth_service.dart'; /// メールアドレスの形式を確認する正規表現 final RegExp _emailRegExp = RegExp( r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', ); /// ログイン画面。 /// /// この画面でユーザーは自分のメールアドレスとパスワードを使用してログインする。 class LoginScreen extends StatelessWidget { final TextEditingController _emailController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final AuthService _authService = AuthService(); LoginScreen({Key? key}) : super(key: key); /// ログインする。 Future<void> _signIn( ScaffoldMessengerState scaffoldMessenger, BuildContext context, ) async { final email = _emailController.text.trim(); final password = _passwordController.text; if (!_validateFields(email, password, scaffoldMessenger)) { return; } try { final errorMsg = await _authService.signInWithEmailAndPassword( email, password, ); if (errorMsg == null) { goRouter.go(homePath); } else { _showErrorSnackBar(scaffoldMessenger, errorMsg); } } on FirebaseAuthException catch (e) { _showErrorSnackBar(scaffoldMessenger, e.message ?? 'エラーが発生しました。'); } } void _showErrorSnackBar( ScaffoldMessengerState scaffoldMessenger, String message) { scaffoldMessenger.showSnackBar(SnackBar(content: Text(message))); } bool _validateFields( String email, String password, ScaffoldMessengerState scaffoldMessenger) { if (email.isEmpty || password.isEmpty) { _showErrorSnackBar(scaffoldMessenger, 'すべてのフィールドを入力してください。'); return false; } if (!_emailRegExp.hasMatch(email)) { _showErrorSnackBar(scaffoldMessenger, '有効なメールアドレスを入力してください。'); return false; } if (password.length < 6) { _showErrorSnackBar(scaffoldMessenger, 'パスワードは6文字以上である必要があります。'); return false; } return true; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('ログイン')), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ TextField( controller: _emailController, decoration: const InputDecoration(labelText: 'メールアドレス'), ), TextField( controller: _passwordController, decoration: const InputDecoration(labelText: 'パスワード'), obscureText: true, ), ElevatedButton( onPressed: () { final scaffoldMessenger = ScaffoldMessenger.of(context); _signIn(scaffoldMessenger, context); }, child: const Text('ログイン'), ), TextButton( onPressed: () => goRouter.go(signupPath), child: const Text('新規登録'), ), ], ), ), ); } }
説明
LoginScreen
クラス
- ログイン画面を表示するウィジェットです。
_signIn
メソッド
- ユーザーが入力した情報を用いてログインを行います。成功した場合はホーム画面に遷移し、失敗した場合はエラーメッセージを表示します。
_validateFields
メソッド
- 入力フィールドのバリデーションを行います。
ホーム画面(ログイン後の画面)
編集:lib/screens/home_screen.dart
import 'package:flutter/material.dart'; import '../services/auth_service.dart'; import '../routers/router.dart'; /// ホーム画面。 /// /// ログイン後に遷移する主要な画面。 class HomeScreen extends StatelessWidget { HomeScreen({Key? key}) : super(key: key); final AuthService _authService = AuthService(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('ホーム')), body: Center( child: ElevatedButton( onPressed: () async { await _authService.signOut(); goRouter.go(loginPath); }, child: const Text('ログアウト'), ), ), ); } }
説明
HomeScreen
クラス
- ログイン後に表示されるホーム画面です。
ElevatedButton
- ログアウトボタン。クリックすると
AuthService
のsignOut
メソッドが呼び出され、ユーザーがログアウトされます。
動作確認
エミュレーターでの画面遷移の確認
以下を確認しました。
新規登録画面でメールアドレスとパスワードを入力して「新規登録」ボタンを押すと、 ホーム画面に遷移する。
ホーム画面で「ログアウト」ボタンを押すと、ログイン画面に遷移する。
ログイン画面でメールアドレスとパスワードを入力して「ログイン」ボタンを押すと、ホーム画面に遷移する。
Firebase コンソールでの確認
Firebase コンソールで Authentication のページを確認します。
このようにメール/パスワード認証したアカウント登録が完了しています。
おわりに
今回は、 Flutter アプリに Firebase プロダクトの Authentication を導入する手順についてまとめました。
ありがとうございました。
関連記事
エディタのテーマを変更してみたけど、めっちゃ気分転換になった。 pic.twitter.com/QxPygXkyUy
— やまて|ソフトウェアエンジニア2年目 (@r_yamate) 2023年9月11日
開きたくなるエディタになりました。