🎃

[Flutter x Firebase] カウンターアプリに認証機能を追加する

2020/10/30に公開

やること

  • FlutterとFirebaseの連携方法や、使い方などを学ぶ。今回はFirebase Authentication。
  • あくまでFirebaseの学習がメインなので、デフォルトで作成されるカウンターアプリ部分にはなるべく手を加えない。
  • iOSのみ
  • Googleアカウントでのサインインのみ(他のは少し手間がかかりそうだったので)

環境

  • Flutter 1.22
  • VSCode

成果物

動作イメージ

image.png

ソースコード

https://github.com/popy1017/firebase_counter_app/tree/v0.1

準備

詳細は割愛。注意するところのみ記載。

0. カウンターアプリを作る

flutter create special_counter_app

以上。

1. Firebaseのプロジェクトを作る

Firebaseコンソールにログインして、「プロジェクトの作成」から新規プロジェクトを作成する。

以上。

2. Flutter アプリにFirebaseを追加する

参考ページ → Flutter アプリに Firebase を追加する
Flutterを使ったことがあれば、Step1は終わっているはず。
上でStep2(Firebaseプロジェクトの作成)もやっているので、Step3からスタート。

FirebaseコンソールのプロジェクトページにあるiOSボタン(以下参照)を押して、指示に従う。
image.png

アプリの登録

Bundle IDが必要なので、XCodeで設定・確認しておく必要がある。

設定ファイルのダウンロード

GoogleService-Info.plistをダウンロードして、.gitignoreに追加して、ios/Runner配下に配備する。

.gitignore
# Firebase related
ios/Runner/GoogleService-Info.plist

Firebase SDKの追加

ios/配下でpod initを実行し、以下を追記。
追記後、pod installを実行。

Podfile
# add the Firebase pod for Google Analytics
pod 'Firebase/Analytics'
# add pods for any other desired Firebase products
# https://firebase.google.com/docs/ios/setup#available-pods

初期化コードの追加

AppDelegate.swiftに 以下の3行を追加

AppDelegate.swift
import UIKit
import Flutter
import Firebase // <- 追加
import FirebaseCore // <- 追加

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    FirebaseApp.configure()  // <- 追加
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

公式サイトの説明ではimport FirebaseCoreは含まれていなかったが、FirebaseAppFirebaseCoreで定義されているようなので必須。
ios - Cannot import Firebase in Swift app - Stack Overflow

アプリを実行してインストールを確認

エラー対処

上記の手順を実行して、いざVSCodeで実行すると なんのエラーも出ずにアプリが落ちてしまう現象が起こった。
XCodeで実行してみると、以下のようなエラーが出ていたことが判明。

libc++abi.dylib: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'com.firebase.core', reason: '`[FIRApp configure];`
 (`FirebaseApp.configure()` in Swift) could not find a valid GoogleService-Info.plist 
in your project. Please download one from https://console.firebase.google.com/.'
terminating with uncaught exception of type NSException

GoogleService-Info.plistがないよ と言われているが、上の手順で追加したはず。。。
調べてみると、XCodeのプロジェクトに認識されていないようで、XCode上で追加する必要があった。

プロジェクトで有効なGoogleService-Info.plistが見つかりませんでした

XCodeの「File」メニューから「Add Files to "Runner" ...」を選択して、GoogleService-Info.plistを追加する。

確認中が終わらない

原因は掴めなかったが、flutter cleanや実行を繰り返していたら 完了になった。

以上で、FlutterアプリとFirebaseの連携は完了。

Authentication を使う

まず、以下のような画面フローにするため、ログイン画面を先に作る。

image.png

ログイン画面を作る

今回は、AppleIDやGoogleアカウントなどを使ってログインできる仕様にするため、それぞれのサインインボタンを追加する。
パッケージ sign_button を使うと、あらかじめデザインされたサインインボタンを簡単に作ることができる。
lib/pages/login_form.dartを作成し、以下のようにした。
それっぽく?するために、PaddingCardを使っているが、中身はColumnSignInButtonを並べているだけ。

login_form.dart
import 'package:flutter/material.dart';
import 'package:sign_button/sign_button.dart';

class LoginForm extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ログイン'),
      ),
      body: Center(
        child: Card(
          child: Padding(
            padding: const EdgeInsets.symmetric(
              vertical: 100,
              horizontal: 50,
            ),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                SignInButton(
                  buttonType: ButtonType.apple,
                  onPressed: () {
                    print('click');
                  },
                ),
                SignInButton(
                  buttonType: ButtonType.google,
                  onPressed: () {
                    print('click');
                  },
                ),
                SignInButton(
                  buttonType: ButtonType.twitter,
                  onPressed: () {
                    print('click');
                  },
                ),
                SignInButton(
                  buttonType: ButtonType.github,
                  onPressed: () {
                    print('click');
                  },
                ),
                SignInButton(
                  buttonType: ButtonType.yahoo,
                  onPressed: () {
                    print('click');
                  },
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}

image.png

ログイン処理を書いていく

今回実装する Google、Appleなどのアカウントを使ったログイン方法ソーシャルログインのやり方はこちら → Social Authentication | FlutterFire

準備:パッケージのインストール

package.yaml
~~
dependencies:
  flutter:
    sdk: flutter
  
  # ソーシャルログインボタンのデザインセット
  sign_button: ^1.0.0

  # Firebase related
  firebase_core: ^0.5.0+1
  firebase_auth: ^0.18.1+2
  google_sign_in: ^4.5.5

  # 最前面にローディング(くるくる)を簡単に出すパッケージ
  flutter_easyloading: ^2.0.0

  # 状態管理で使用
  provider: ^4.3.2+2
~~

実装時のポイント

  • ログイン状態の管理
  • ログインの実装(Google)
  • ログアウトの実装
  • ロード中(くるくる)の表示

ログイン状態の管理

今回はログイン画面(未ログイン状態)とカウンター画面(ログイン状態)で状態を共有する必要があるので、Providerパターンを使って状態を共有する。

状態管理クラスの作成

まず lib/models/auth_model.dartを作って 状態を保持するクラスとしてChangeNotifierを継承したクラスAuthModelを以下のようにした。

auth_model.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';

class AuthModel extends ChangeNotifier {
  User _user;
  User get user => _user;

  // ログイン処理
  Future<void> login() async {}  

  // ログアウト処理
  Future<void> logout() async {} 
}
状態管理クラスをProvideする

このままでは ログイン画面やカウンター画面はAuthModelのことを知らないので、ChangeNotifierProviderを使って知らせてあげる必要がある。
今回はそれぞれ入れ子になっていない2つの画面に渡す必要があるので、main.dartMaterialAppChangeNotifierProviderで囲む必要がある。

main.dart
class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return ChangeNotifierProvider( // <== 追加
      create: (context) => AuthModel(), // <== 追加
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: LoginForm(),
      ),
    );
  }
}
状態管理クラスの関数を呼ぶ

ログイン画面とカウンター画面はAuthModelを認識することができたので、context.read<AuthModel>().login()などでログイン・ログアウト関数を呼び出すことができる。

login_form.dart
class LoginForm extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ログイン'),
      ),
      body: _buildSocialLogin(context),
    );
  }

  Center _buildSocialLogin(BuildContext context) {
    return Center(
      child: Card(
        child: Padding(
          padding: const EdgeInsets.symmetric(
            vertical: 100,
            horizontal: 50,
          ),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              ~~~
              SignInButton(
                buttonType: ButtonType.google,
                onPressed: () async {
                  await context.read<AuthModel>().login(); // <== ログイン
                },
              ),
              ~~~
            ],
          ),
        ),
      ),
    );
  }
}

ログイン・ログアウトの実装

Social Authentication | FlutterFireを参考に、以下のように実装。
以下のコードではGoogleアカウントでしかサインインできないので、他の種類のアカウントでログインしたい場合はlogin()関数内で種類に応じて振り分ける必要がある。

auth_model.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';

class AuthModel extends ChangeNotifier {
  User _user;

  final FirebaseAuth _auth = FirebaseAuth.instance;

  User get user => _user;

  Future<bool> login() async {
    try {
      UserCredential _userCredential = await _signInWithGoogle();
      _user = _userCredential.user;
      notifyListeners();
      return true;
    } catch (error) {
      print(error);
      return false;
    }
  }

  Future<void> logout() async {
    _user = null;
    await _auth.signOut();
    notifyListeners();
  }

  Future<UserCredential> _signInWithGoogle() async {
    // Trigger the authentication flow
    final GoogleSignInAccount googleUser = await GoogleSignIn().signIn();

    // Obtain the auth details from the request
    final GoogleSignInAuthentication googleAuth =
        await googleUser.authentication;

    // Create a new credential
    final GoogleAuthCredential credential = GoogleAuthProvider.credential(
      accessToken: googleAuth.accessToken,
      idToken: googleAuth.idToken,
    );

    // Once signed in, return the UserCredential
    return await FirebaseAuth.instance.signInWithCredential(credential);
  }
}

ロード中(くるくる)の表示

ログイン処理中に何も表示されないのも味気ないので、ローディングインジケーターを表示する。

こちらのパッケージを使う → firebase_auth | Flutter Package

準備

使い方にも書いてあるが、常に最前面に出すためにMaterialAppbuilderFlutterEasyLoading()を仕込む必要がある。

main.dart
class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => AuthModel(),
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: LoginForm(),
        /* 追加 */
        builder: (BuildContext context, Widget child) {
          /// make sure that loading can be displayed in front of all other widgets
          return FlutterEasyLoading(child: child);
        },
      ),
    );
  }
}
使い方

EasyLoading.show(status: 文字列)でローディングを表示
EasyLoading.dismiss()でローディングを閉じる。
AuthModelのログイン処理を以下のようにラップすれば ログイン処理中にローディングが表示されるようになる。

login_form.dart
  Future<void> _login(BuildContext context) async {
    EasyLoading.show(status: 'loading...');
    if (await context.read<AuthModel>().login()) {
      Navigator.pushAndRemoveUntil(
        context,
        MaterialPageRoute(builder: (_) => MyHomePage(title: "カウンター")),
        (_) => false,
      );
    }
    EasyLoading.dismiss();
  }

(おまけ)カウンター画面にアカウント情報を表示する

main.dart
class _MyHomePageState extends State<MyHomePage> {
  
  ~~

  
  Widget build(BuildContext context) {
    return Scaffold(
      ~~
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            _buildAccountInfo(),
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      ~~
    );
  }
  
  Widget _buildAccountInfo() {
    final User _user = context.select((AuthModel _auth) => _auth.user);

    // ログアウト直後に _user を null にしており、_user.photoURLでエラーが出るため分岐させている
    return _user != null
        ? Card(
            child: ListTile(
              // 丸いアバターを表示
              leading: CircleAvatar(
                backgroundImage: NetworkImage(_user.photoURL ?? ''),
              ),
              title: Text(_user.displayName),
              subtitle: Text(_user.email),
            ),
          )
        : Container(); 
  }
  ~~
}

エラー記録

module 'firebase_auth not found"

Podfile, Podfile.lockを削除して、flutter clean
その後、flutter runでOK。
参考: xcode - module 'firebase_auth not found" - Stack Overflow

No Firebase App '[DEFAULT]' has been created - call Firebase.initializeApp()

Firebaseの機能を使う前にinitializeAppを読んでくださいねっていうエラー。
以下の記事でどこで呼ぶのが良さそうかを解説してくれている。
参考: FlutterでFirebaseを使うときのFirebase.initializeApp()の呼び方 - Qiita

  • FutureBuilder
  • StatefulWidgetinitState
  • runAppの前 ← 今回はこれを採用
main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized(); // <- 追加
  await Firebase.initializeApp(); // <- 追加
  runApp(MyApp());
}

Discussion