snovaのブログ

主にプログラミングやデジタルコンテンツについて書きます。最近はPython, Flutter, VRに興味があります。

Flutterから利用できるFirebaseサービスをカウンターアプリで実践(Authentication編)

はじめに

Flutterから利用できるFirebaseの機能をカウンターアプリに実装し、内容をまとめたシリーズの第5回目です。

今回はユーザー認証機能を使うことができるFirebase Authenticationについてです。

目次

シリーズの内容

回数 内容 リンク
第0回 準備編 ブログ
第1回 Analytics ブログ
第2回 Firebase Crashlytics ブログ
第3回 Firebase Performance Monitoring ブログ
第4回 Firebase Remote Config ブログ
第5回 Firebase Authentication イマココ
第6回 Cloud Firestore ブログ
第7回 Firebase Realtime Database ブログ
第8回 Cloud Storage for Firebase ブログ
第9回 Firebase Cloud Messaging ブログ
第10回 Firebase In-App Messaging ブログ
第11回 Firebase ML ブログ
第12回 Cloud Functions for Firebase ブログ
第13回 Firebase Hosting ブログ
第14回 Firebaseのその他のサービス ブログ

開発環境

項目 内容
PC Macbook Air(M1)
Flutter 3.0.1
Firebase 11.0.1
FlutterFire 0.2.2+2
デバッグバイス Android 12(APIレベル31), Chrome

準備

準備編が完了出来ているものとします。

導入方法

プロジェクトにfirebase_authを導入するため、pubspec.yamlに追記しインポートします。

dependencies:
  firebase_auth: ^3.3.18

ログインの方法(メールアドレス、電話番号など)を選択するため、Firebase ConsoleからAuthenticationへ移動し、Sign-in Methodで希望するログイン方法を選びます。 今回は、メールアドレスとパスワードでの設定を行います。

Firebase Consoleでの設定が終わったら、実装していきます。

TextFieldに入力したメールアドレスとパスワードを入力します。 パスワードはobscureTexttrueにして、見えないようにします。

/// メールアドレス入力
TextField(
  decoration: const InputDecoration(
    label: Text('E-mail'),
  ),
  controller: _idController,
),

/// パスワード入力
TextField(
  decoration: const InputDecoration(
    label: Text('Password'),
  ),
  controller: _passController,
  obscureText: true,
),

実行ボタンを作り、アカウント作成またはサインインできる関数を呼びます。

/// アカウント作成の場合
Container(
  margin: const EdgeInsets.all(10),
  child: ElevatedButton(
    onPressed: () {
      _createAccount(ref, idController.text, passController.text);
    },
    child: const Text('アカウント作成'),
  ),
),

アカウント作成の処理は、FirebaseAuth.instance.createUserWithEmailAndPasswordを使います。 メールアドレスとパスワードを渡し、エラーが発生したらエラーメッセージを出すようにします。

import 'package:firebase_auth/firebase_auth.dart';

void _createAccount(String id, String pass) async {
  try {
    /// credential にはアカウント情報が記録される
    final credential =
        await FirebaseAuth.instance.createUserWithEmailAndPassword(
      email: id,
      password: pass,
    );
  }

  /// アカウントに失敗した場合のエラー処理
  on FirebaseAuthException catch (e) {
    /// パスワードが弱い場合
    if (e.code == 'weak-password') {
      print('パスワードが弱いです');

      /// メールアドレスが既に使用中の場合
    } else if (e.code == 'email-already-in-use') {
      print('すでに使用されているメールアドレスです');
    }

    /// その他エラー
    else {
      print('アカウント作成エラー');
    }
  } catch (e) {
    print(e);
  }
}

サインインの処理は、FirebaseAuth.instance.signInWithEmailAndPasswordを使います。 メールアドレスとパスワードを渡し、エラーが発生したらエラーメッセージを出すようにします。

void _signIn(String id, String pass) async {
  try {
    /// credential にはアカウント情報が記録される
    final credential = await FirebaseAuth.instance.signInWithEmailAndPassword(
      email: id,
      password: pass,
    );
  }

  /// サインインに失敗した場合のエラー処理
  on FirebaseAuthException catch (e) {
    /// メールアドレスが無効の場合
    if (e.code == 'invalid-email') {
      print('メールアドレスが無効です');
    }

    /// ユーザーが存在しない場合
    else if (e.code == 'user-not-found') {
      print('ユーザーが存在しません');
    }

    /// パスワードが間違っている場合
    else if (e.code == 'wrong-password') {
      print('パスワードが間違っています');
    }

    /// その他エラー
    else {
      print('サインインエラー');
    }
  }
}

サインアウトFirebaseAuth.instance.signOut()を使用します。

void _signOut() async {
  await FirebaseAuth.instance.signOut();
}

ユーザーの情報を取得するためには、3通りの方法があります。

/// authStateChanges、idTokenChanges、およびuserChangesストリームを使う
FirebaseAuth.instance
  .authStateChanges()
  .listen((User? user) {
    if (user != null) {
      print(user.uid);
    }
  });

/// 認証(signIn)メソッドによって返されるUserCredentialオブジェクトを使う
final userCredential =
    await FirebaseAuth.instance.signInWithCredential(credential);
final user = userCredential.user;
print(user?.uid);

/// FirebaseAuthインスタンスのcurrentUserプロパティを使う
if (FirebaseAuth.instance.currentUser != null) {
  print(FirebaseAuth.instance.currentUser?.uid);
}

ユーザーのプロファイルやメールアドレスを更新するには、.update***を使用します。

final userCredential =
    await FirebaseAuth.instance.signInWithCredential(credential);
final user = userCredential.user;

await user?.updateDisplayName("Jane Q. User");
await user?.updateEmail("janeq@example.com");

メールアドレスでの認証だけではなく、電話番号での認証や、OAuthでの認証もあります。

サインイン前のホーム画面

認証画面

サインイン後のホーム画面

コード全文

前回からの変更点です。

  • Firebase Authを実装
  • サインページを追加
  • その他、コードの変更

main.dart

/// Flutter関係のインポート
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:async';

/// Firebase関係のインポート
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_auth/firebase_auth.dart';

/// 他ページのインポート
import 'package:counter_firebase/normal_counter_page.dart';
import 'package:counter_firebase/crash_page.dart';
import 'package:counter_firebase/auth_page.dart';
import 'package:counter_firebase/remote_config_page.dart';

/// メイン
void main() async {
  /// クラッシュハンドラ
  runZonedGuarded<Future<void>>(() async {
    /// Firebaseの初期化
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    /// クラッシュハンドラ(Flutterフレームワーク内でスローされたすべてのエラー)
    FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;

    /// runApp w/ Riverpod
    runApp(const ProviderScope(child: MyApp()));
  },

      /// クラッシュハンドラ(Flutterフレームワーク内でキャッチされないエラー)
      (error, stack) =>
          FirebaseCrashlytics.instance.recordError(error, stack, fatal: true));
}

/// Providerの初期化
/// カウンター用のプロバイダー
final counterProvider = StateNotifierProvider.autoDispose<Counter, int>((ref) {
  return Counter();
});

class Counter extends StateNotifier<int> {
  Counter() : super(0);

  /// カウントアップ
  void increment() => state++;
}

/// MaterialAppの設定
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Counter Firebase',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

/// ホーム画面
class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    FirebaseAuth.instance.authStateChanges().listen((User? user) {
      if (user == null) {
        ref.watch(userEmailProvider.state).state = 'ログインしていません';
      } else {
        ref.watch(userEmailProvider.state).state = user.email!;
      }
    });

    return Scaffold(
      appBar: AppBar(
        title: const Text('My Homepage'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(10),
        children: <Widget>[
          /// ユーザ情報の表示
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Icon(Icons.person),
              Text(ref.watch(userEmailProvider)),
            ],
          ),

          /// 各ページへの遷移
          _PagePushButton(context, 'ノーマルカウンター', const NormalCounterPage()),
          _PagePushButton(context, 'クラッシュページ', const CrashPage()),
          _PagePushButton(
              context, 'Remote Configカウンター', const RemoteConfigPage()),
          _PagePushButton(context, '認証ページ', const AuthPage()),
        ],
      ),
    );
  }
}

/// ページ遷移ボタン
class _PagePushButton extends Container {
  _PagePushButton(BuildContext context, String buttonTitle, pagename)
      : super(
          padding: const EdgeInsets.all(10),
          child: ElevatedButton(
            child: Text(buttonTitle),
            onPressed: () {
              AnalyticsService().logPage(buttonTitle);
              Navigator.push(
                  context, MaterialPageRoute(builder: (context) => pagename));
            },
          ),
        );
}

/// Analyticsの実装
class AnalyticsService {
  /// ページ遷移のログ
  Future<void> logPage(String screenName) async {
    await FirebaseAnalytics.instance.logEvent(
      name: 'screen_view',
      parameters: {
        'firebase_screen': screenName,
      },
    );
  }
}

auth_page.dart

/// Flutter
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

/// Firebase関係のインポート
import 'package:firebase_auth/firebase_auth.dart';

/// Other Page
import 'package:counter_firebase/main.dart';

/// Authのサインイン状態のprovider
final signInStateProvider = StateProvider((ref) => 'サインインまたはアカウントを作成してください');

/// サインインユーザーの情報プロバイダー
final userProvider = StateProvider<User?>((ref) => null);
final userEmailProvider = StateProvider<String>((ref) => 'ログインしていません');

/// ページ設定
class AuthPage extends ConsumerStatefulWidget {
  const AuthPage({Key? key}) : super(key: key);

  @override
  AuthPageState createState() => AuthPageState();
}

class AuthPageState extends ConsumerState<AuthPage> {
  @override
  void initState() {
    super.initState();

    /// 現在のユーザー情報
    print(FirebaseAuth.instance.currentUser?.displayName);
    print(FirebaseAuth.instance.currentUser?.email);
  }

  @override
  Widget build(BuildContext context) {
    final singInStatus = ref.watch(signInStateProvider);
    final idController = TextEditingController();
    final passController = TextEditingController();

    return Scaffold(
      appBar: AppBar(
        title: const Text('Auth Page'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(10),
        children: <Widget>[
          /// メールアドレス入力
          TextField(
            decoration: const InputDecoration(
              label: Text('E-mail'),
              icon: Icon(Icons.mail),
            ),
            controller: idController,
          ),

          /// パスワード入力
          TextField(
            decoration: const InputDecoration(
              label: Text('Password'),
              icon: Icon(Icons.key),
            ),
            controller: passController,
            obscureText: true,
          ),

          /// サインイン
          Container(
            margin: const EdgeInsets.all(10),
            child: ElevatedButton(
              onPressed: () {
                /// ログインの場合
                _signIn(ref, idController.text, passController.text);
              },
              style: ButtonStyle(
                  backgroundColor: MaterialStateProperty.all(Colors.grey)),
              child: const Text('サインイン'),
            ),
          ),

          /// アカウント作成
          Container(
            margin: const EdgeInsets.all(10),
            child: ElevatedButton(
              onPressed: () {
                /// アカウント作成の場合
                _createAccount(ref, idController.text, passController.text);
              },
              child: const Text('アカウント作成'),
            ),
          ),

          /// ログイン状態の表示
          Container(
            padding: const EdgeInsets.all(10),
            child: Text('メッセージ : $singInStatus'),
          ),

          /// サインアウト
          TextButton(
              onPressed: () {
                _signOut(ref);
              },
              child: const Text('SIGN OUT'))
        ],
      ),
    );
  }
}

/// サインイン処理
void _signIn(WidgetRef ref, String id, String pass) async {
  try {
    /// credential にはアカウント情報が記録される
    final credential = await FirebaseAuth.instance.signInWithEmailAndPassword(
      email: id,
      password: pass,
    );

    /// ユーザ情報の更新
    ref.watch(userProvider.state).state = credential.user;
    print(credential);

    /// 画面に表示
    ref.read(signInStateProvider.state).state = 'サインインできました!';
  }

  /// サインインに失敗した場合のエラー処理
  on FirebaseAuthException catch (e) {
    /// メールアドレスが無効の場合
    if (e.code == 'invalid-email') {
      ref.read(signInStateProvider.state).state = 'メールアドレスが無効です';
    }

    /// ユーザーが存在しない場合
    else if (e.code == 'user-not-found') {
      ref.read(signInStateProvider.state).state = 'ユーザーが存在しません';
    }

    /// パスワードが間違っている場合
    else if (e.code == 'wrong-password') {
      ref.read(signInStateProvider.state).state = 'パスワードが間違っています';
    }

    /// その他エラー
    else {
      ref.read(signInStateProvider.state).state = 'サインインエラー';
    }
  }
}

/// アカウント作成
void _createAccount(WidgetRef ref, String id, String pass) async {
  try {
    /// credential にはアカウント情報が記録される
    final credential =
        await FirebaseAuth.instance.createUserWithEmailAndPassword(
      email: id,
      password: pass,
    );

    /// ユーザ情報の更新
    ref.watch(userProvider.state).state = credential.user;
    print(credential);

    /// 画面に表示
    ref.read(signInStateProvider.state).state = 'アカウント作成に成功しました!';
  }

  /// アカウントに失敗した場合のエラー処理
  on FirebaseAuthException catch (e) {
    /// パスワードが弱い場合
    if (e.code == 'weak-password') {
      ref.read(signInStateProvider.state).state = 'パスワードが弱いです';

      /// メールアドレスが既に使用中の場合
    } else if (e.code == 'email-already-in-use') {
      ref.read(signInStateProvider.state).state = 'すでに使用されているメールアドレスです';
    }

    /// その他エラー
    else {
      ref.read(signInStateProvider.state).state = 'アカウント作成エラー';
    }
  } catch (e) {
    print(e);
  }
}

/// サインアウト
void _signOut(WidgetRef ref) async {
  await FirebaseAuth.instance.signOut();
  ref.read(signInStateProvider.state).state = 'サインインまたはアカウントを作成してください';
}

GitHubのページを貼ります。

github.com

参考

www.flutter-study.dev

zenn.dev

Google Play and the Google Play logo are trademarks of Google LLC.