転職したらスマレジだった件

スマレジのエンジニアやまてのテックブログです。マジレス大歓迎です。

Flutter アプリに Firebase Authentication を導入する手順

スマレジの テックファーム(SES 部門) でWebエンジニアとして働いている やまて(@r_yamate) と申します。

実務では 2023 年 3 月末まで、 SES の派遣先のテーブルオーダーシステムの機能改修の設計などを担当しました。

2023 年 4 月からは、スマレジの関連アプリの開発業務を担当しています。触ったことのなかった Flutter での開発で、日々奮闘中です。

はじめに

今回は、 Flutter アプリに Firebase プロダクトの Authentication を導入する手順についてまとめます。

以前投稿した「Flutter アプリに Firebase を導入する手順」を一つ前のステップとしている関連記事です。

ryamate.hatenablog.com

目次

環境

Firebase Authentication とは

Firebase Authenticationは、安全な認証システムを簡単に構築できるサービスです。

公式ドキュメントでの説明は以下のとおりです。

Firebase Authentication は、安全な認証システムの構築を容易にし、エンドユーザーのログインや初期登録のエクスペリエンスを向上させることを目的としています。メールアドレスとパスワードの組み合わせ、電話認証、GoogleTwitterFacebookGitHub のログインなどに対応したエンドツーエンドの ID ソリューションです。

firebase.google.com

firebase.google.com

今回は手始めにユーザーがメールアドレスとパスワードを使用して Firebase での認証ができるようにします。(GoogleGitHub でのログインなどにも対応できるとのことでいつかはやってみたいです)

firebase.google.com

1. Firebase コンソールでの操作

1-1. Authentication を設定する

まずは、 Firebase コンソール で、左側のメニューから「Authentication」を選択します。

「始める」ボタンをクリックします。

1-2. メール/パスワード認証を有効にする

次に、「Sign-in method」(サインイン方法)タブを選択し、「メール/パスワード」の行の編集アイコンをクリックします。

メール/パスワード認証を有効化します。

設定が完了したら、「保存」ボタンをクリックします。

これで、 Firebase Authentication の初期設定は完了です。

2. プラグイン追加とコマンドの実行

※ 以下の記事の 3. Firebase の初期化とプラグインの追加 の手順まで実施した状態である必要があります。

ryamate.hatenablog.com

2-1. プラグイン firebase_auth の追加

まず、 firebase_auth プラグインをプロジェクトに追加します。このプラグインは、 Firebase Authentication の Flutter 用のライブラリです。

pub.dev

編集: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

firebase.google.com

編集: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

  • ログアウトボタン。クリックするとAuthServicesignOutメソッドが呼び出され、ユーザーがログアウトされます。

動作確認

エミュレーターでの画面遷移の確認

以下を確認しました。

  • 新規登録画面でメールアドレスとパスワードを入力して「新規登録」ボタンを押すと、 ホーム画面に遷移する。

  • ホーム画面で「ログアウト」ボタンを押すと、ログイン画面に遷移する。

  • ログイン画面でメールアドレスとパスワードを入力して「ログイン」ボタンを押すと、ホーム画面に遷移する。

Firebase コンソールでの確認

Firebase コンソールで Authentication のページを確認します。

このようにメール/パスワード認証したアカウント登録が完了しています。

おわりに

今回は、 Flutter アプリに Firebase プロダクトの Authentication を導入する手順についてまとめました。

ありがとうございました。

関連記事

ryamate.hatenablog.com

ryamate.hatenablog.com



開きたくなるエディタになりました。