[Flutter x Firebase] カウンターアプリに認証機能を追加する
やること
- FlutterとFirebaseの連携方法や、使い方などを学ぶ。今回はFirebase Authentication。
- あくまでFirebaseの学習がメインなので、デフォルトで作成されるカウンターアプリ部分にはなるべく手を加えない。
- iOSのみ
- Googleアカウントでのサインインのみ(他のは少し手間がかかりそうだったので)
環境
- Flutter 1.22
- VSCode
成果物
動作イメージ
ソースコード
準備
詳細は割愛。注意するところのみ記載。
0. カウンターアプリを作る
flutter create special_counter_app
以上。
1. Firebaseのプロジェクトを作る
Firebaseコンソールにログインして、「プロジェクトの作成」から新規プロジェクトを作成する。
以上。
2. Flutter アプリにFirebaseを追加する
参考ページ → Flutter アプリに Firebase を追加する
Flutterを使ったことがあれば、Step1は終わっているはず。
上でStep2(Firebaseプロジェクトの作成)もやっているので、Step3からスタート。
FirebaseコンソールのプロジェクトページにあるiOSボタン(以下参照)を押して、指示に従う。
アプリの登録
Bundle IDが必要なので、XCodeで設定・確認しておく必要がある。
設定ファイルのダウンロード
GoogleService-Info.plist
をダウンロードして、.gitignore
に追加して、ios/Runner
配下に配備する。
# Firebase related
ios/Runner/GoogleService-Info.plist
Firebase SDKの追加
ios/
配下でpod init
を実行し、以下を追記。
追記後、pod install
を実行。
# 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行を追加
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
は含まれていなかったが、FirebaseApp
はFirebaseCore
で定義されているようなので必須。
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 を使う
まず、以下のような画面フローにするため、ログイン画面を先に作る。
ログイン画面を作る
今回は、AppleIDやGoogleアカウントなどを使ってログインできる仕様にするため、それぞれのサインインボタンを追加する。
パッケージ sign_button を使うと、あらかじめデザインされたサインインボタンを簡単に作ることができる。
lib/pages/login_form.dart
を作成し、以下のようにした。
それっぽく?するために、Padding
やCard
を使っているが、中身はColumn
でSignInButton
を並べているだけ。
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');
},
)
],
),
),
),
),
);
}
}
ログイン処理を書いていく
今回実装する Google、Appleなどのアカウントを使ったログイン方法ソーシャルログイン
のやり方はこちら → Social Authentication | FlutterFire
準備:パッケージのインストール
~~
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
を以下のようにした。
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.dart
のMaterialApp
をChangeNotifierProvider
で囲む必要がある。
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()
などでログイン・ログアウト関数を呼び出すことができる。
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()
関数内で種類に応じて振り分ける必要がある。
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
準備
使い方にも書いてあるが、常に最前面に出すためにMaterialApp
のbuilder
にFlutterEasyLoading()
を仕込む必要がある。
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
のログイン処理を以下のようにラップすれば ログイン処理中にローディングが表示されるようになる。
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();
}
(おまけ)カウンター画面にアカウント情報を表示する
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
-
StatefulWidget
のinitState
-
runApp
の前 ← 今回はこれを採用
void main() async {
WidgetsFlutterBinding.ensureInitialized(); // <- 追加
await Firebase.initializeApp(); // <- 追加
runApp(MyApp());
}
Discussion