Flutter から Firebase を使う
Firebase のユーザ認証システムを Web アプリから使うという記事を前に出しました.
今度は、モバイルアプリ向けということで、同じ Firebase 認証システムを Flutter アプリから使ってみます.
最初に Firebase コンソールで、アプリケーションの登録をします。
前の記事で、Web アプリを登録していますのでそちらも参照ください。 同じ場所に追加で、 Android と iOS アプリを登録します。
開発中は、あまりアプリの ID 等は気にしないと思いますが、Firebase に登録する時には、きちんと決めておく必要があります。 一度登録したら変更できませんので (登録しているものを消して新しく作り直すとか面倒な作業が必要です)、考えてから設定してください。
通常、アプリ ID は、FQDN (Fully Qualified Domain Name) を逆読みしたものを使います。
com.techaas.flutter.firebase_auth
のように、FQDN に プラットフォーム + アプリ名のようにして、重複しない様にします。

Firebase コンソールでアプリを追加すると構成ファイルがダウンロードできるようになります。
Android版の方は、ダウンロードしたものをandroid/app/google-services.json
に配置しておきます。
iOS版はios/Runner/GoogleService-Info.plist
に配置します。
構成ファイルは間違って公開されないように、.gitignore
などに追加しておきましょう。
Flutter から Firebase を使うための準備ですが、少しずつですが、色々なファイルを変更する必要があります。
android/build.gradle
に classpath
を追加します。
buildscript {
...
dependencies {
...
classpath 'com.google.gms:google-services:4.3.8' // Google Services plugin
}
}
android/app/build.gradle
で plugin を使えるようにします。
apply plugin: 'com.google.gms.google-services' // Google Services plugin
あと、同じファイルの applicationId
を Firebase コンソールで登録した ID に合わせておいて下さい。
Flutter でパッケージを追加します。pubspec.yaml
に以下を追加します。
dependencies:
flutter:
sdk: flutter
...
firebase_core: ^1.4.0
firebase_analytics: ^8.2.0
firebase_auth: ^3.0.1
flutter_signin_button: ^2.0.0
http: ^0.13.3
パッケージを追加したら、pub get
して、追加したパッケージをダウンロードしてください。
firebase_
で始まるパッケージは、Firebaseのライブラリです。機能ごとに分かれているので必要なものを追加します。
flutter_signin_button
は、ソーシャルログイン用のボタンを作ってくれるパッケージです。ご自分でデザインするなら必要ありません。
http
は、HTTPのネットワークライブラリでRestful APIなどを使う時に使用します。
今回は、JWT トークンの検証テストをする時に使います。
また、ios/Podfile
の先頭あたりで、最低バージョンを設定します。これをやらないと、firebaeのビルドができませんでした。
platform :ios, '10.0'
iOSの方ですが、Firebaseのパッケージを使うために、pod repo update
が必要でした。
(cocoapod とか詳しくは書きませんが、この辺でハマると、少しネイティブの開発の知識が必要になります)
ソースコードは、以下にありますので、参照してください。
まず、アプリの起動時に Firebase に接続します。
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
WidgetsFlutterBinding.ensureInitialized()
は、Firebase
を初期化する前に必要です。
SignInOutPage
がメイン画面になります。
final FirebaseAuth _auth = FirebaseAuth.instance;
class SignInOutPage extends StatefulWidget {
...
}
class _SignInOutPageState extends State<SignInOutPage> {
User? user;
@override
void initState() {
_auth.userChanges().listen((event) {
debugPrint('user: $event');
setState(() => user = event);
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
margin: EdgeInsets.only(top: 100),
alignment: Alignment.center,
child: (user == null) ? _EmailPasswordForm() : _UserInfoCard(user),
),
);
}
}
initState
の中でログイン変化に合わせてUser
が変化する様にしています。
userChanges()
は、Stream<User?>
を返します。Stream をlisten()
して、そこでsetState()
しています。
ユーザの状態が変化したら、Stream
に変化後のUser
が流れてきますので、それに応じて UI がリビルドされるようになります。
画面の方ですが、user
が null はログインされていなので_EmailPasswordForm
を表示します。
null以外ならログイン済になるので_UserInfoCard
を表示することになります。
ログイン画面の方は、入力フォームでユーザ名とパスワードを入力するテキストと、実際にログインを実行するボタンで構成します。
class _EmailPasswordForm extends StatefulWidget {
@override
State<StatefulWidget> createState() => _EmailPasswordFormState();
}
class _EmailPasswordFormState extends State<_EmailPasswordForm> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
alignment: Alignment.center,
child: const Text(
'ユーザー名とパスワードを入力してください',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
validator: (String? value) {
if (value != null && value.isEmpty) return 'テキストを入力してください';
return null;
},
),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(labelText: 'Password'),
validator: (String? value) {
if (value != null && value.isEmpty) return 'テキストを入力してください';
return null;
},
obscureText: true,
),
Container(
padding: const EdgeInsets.only(top: 16),
alignment: Alignment.center,
child: SignInButton(
Buttons.Email,
text: 'ログイン',
onPressed: () async {
if (_formKey.currentState!.validate()) {
await _signInWithEmailAndPassword();
}
},
),
),
],
),
),
));
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
並んでいるのは普通のUIウジェットなので特に説明はありません。

ログインボタンが押されると、_signInWithEmailAndPassword()
が呼び出されます。
Future<void> _signInWithEmailAndPassword() async {
try {
final User? user = (await _auth.signInWithEmailAndPassword(
email: _emailController.text,
password: _passwordController.text,
))
.user;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${user?.email} でログインしました'),
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('ユーザー名かパスワードが違います'),
),
);
}
}
この部分が実際の認証処理です。
と言っても、入力された結果で、Firebase のsignInWithEmailAndPassword()
を呼び出すだけです。
ネットワークの処理とかユーザの突き合わせ処理とか、全部 Firebase がやってくれますので、アプリとしては
成功するか、失敗してエラーを出したかみるだけです。
ログインに成功すると、上でみた、initState()
内でlisten
しているStream
にUser
が流れますので、
ボタンの処理の方は結果を表示するだけですみます。
Reactive になっているので、処理の単位とコードが明確に分かれるのがわかるかと思います。
その辺がきちんと分割できるので、規模が大きくなっても比較的依存関係とか気にしないでプログラムを書けるのが Flutter の利点でもあります。
ログイン済みになると表示される_UserInfoCard
画面でログアウト処理をします。
class _UserInfoCard extends StatefulWidget {
final User? user;
const _UserInfoCard(this.user);
@override
_UserInfoCardState createState() => _UserInfoCardState();
}
class _UserInfoCardState extends State<_UserInfoCard> {
@override
Widget build(BuildContext context) {
return Column(crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[
Text((widget.user == null)
? 'Not signed in'
: '${widget.user!.isAnonymous ? '匿名ユーザ\n\n' : ''}'
'Email: ${widget.user!.email} (verified: ${widget.user!.emailVerified})\n\n'),
SizedBox(
width: 150,
child: ElevatedButton(
child: const Text('Check Token'),
style: ElevatedButton.styleFrom(
primary: Colors.green,
),
onPressed: () async {
await _checkToken();
}),
),
SizedBox(height: 20),
SizedBox(
width: 150,
child: ElevatedButton(
child: const Text('Logout'),
onPressed: () async {
await _signOut();
}),
),
]);
}
ユーザの情報を表示するのと、ボタンが2つあるだけです。

ログアウトのボタンを押すと、_signOut()
を呼び出して、その中でログアウト処理をします。
Future<void> _signOut() async {
await _auth.signOut();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('ログアウトしました'),
),
);
}
中身は、signOut()
を呼び出すだけです。
ここも同じで、ログアウト後の処理はメッセージ表示だけで、実質はなにもしません。
最後に JWT トークンを取得して、サーバで検証をして、ログイン情報が正しくサーバに渡るか検証します。
ログアウト画面にあるボタンから、_checkToken()
が呼び出されます。
Future<void> _checkToken() async {
String? token = await widget.user?.getIdToken();
if (token == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('トークンが取得できませんでした'),
),
);
return;
}
debugPrint('token: $token');
var response = await http.get(url, headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer $token',
});
print('Response status: ${response.statusCode}');
// print('Response body: ${response.body}');
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('トークンが正しく検証されました'),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('検証に失敗しました'),
),
);
}
}
最初に、widget.user?.getIdToken()
を呼び出して、JWTトークンを取得します。
ログインした状態になっていれば、ここでトークンの文字列が返ってきます。
それを、http.get()のパラメータとして渡します。 Web アプリの時と同じで、
Authorization
ヘッダーに、Bearer
で文字列を指定します。
サーバ側は、トークンを Firebase に問い合わせて、正しければ、HTTP response として、
statusCode
で200
を返す仕様にしていますので、それをチェックします。
(今回の記事ではレスポンスコードだけで見ているので、こういう処理にしているだけです)。
Webアプリの時と同じで、Firebaseをきちんとセットアップして、アプリから使える様にできれば、実際の処理は 1行コードです。
WebアプリとFlutterでモバイルアプリの全部のパターンで同じ様な処理にできるので、ほぼ全てを同じシステムでカバーできるようになっていると思います。
モバイルアプリの方でも、SNS などのソーシャルログインをサポートさせることもできます。その辺は、また時間が取れたらやってみようと思います。