FlutterでFirebase Authenticationを使ってみた

FlutterでFirebase Authenticationを使ったチャットアプリを実装してみたので、そのご紹介です。
2018.08.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

大阪オフィスの山田です。通勤が唯一の運動になっていて運動不足が加速しています。今回は前回のチャットアプリに、Firebase Authenticationを使ってログイン機能をつけてみます。Firebase Authenticationを使うこと自体が初めてです。

今回やること

前回作ったチャットアプリは、誰が投稿したかわからない状態だったので、Firebase Authenticationを使い、ログイン機能を追加して誰の投稿かわかるようにします。ログインにはGoogleアカウントを使用します。こちらのリポジトリを部分的に参考にしています。

完成イメージ

開発環境

flutter doctor

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel beta, v0.5.1, on Mac OS X 10.13.5 17F77, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK 28.0.1)
[✓] iOS toolchain - develop for iOS devices (Xcode 9.4)
[✓] Android Studio (version 3.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[!] VS Code (version 1.26.1)
[✓] Connected devices (1 available)

flutter --version

Flutter 0.5.1 • channel beta • https://github.com/flutter/flutter.git
Framework • revision c7ea3ca377 (3 months ago) • 2018-05-29 21:07:33 +0200
Engine • revision 1ed25ca7b7
Tools • Dart 2.0.0-dev.58.0.flutter-f981f09760

準備

各プラットフォームの設定

Android、iOS、各プラットフォームのプロジェクトをFirebase上に作成し、google-service.jsonGoogleService-Info.plistをプロジェクトに追加します。こちらの記事の「準備」の章で各プラットフォームで設定をしていますのでご参照ください。

Firebase Authenticationの準備

最初にFirebase Authenticationを使えるようにします。Firebase Consoleにログインして、左のペインからAuthenicationを選択します。

Authenticationの「ログイン方法」をクリックしてGoogle認証を使用できるようにします。

プロジェクトのサポートメールが必須なので、メールアドレスを入力します。

AndroidでGoogleアカウント認証を行う場合は、アプリのSHA-1フィンガープリントを設定する必要があります。ターミナルで以下のコマンドを実行して、SHA-1フィンガープリントを表示してください。

keytool -alias androiddebugkey -keystore ~/.android/debug.keystore -list -v -storepass android

Firebase Consoleに戻り、設定からAndroidアプリの設定に移動し、SHA-1フィンガープリントを設定します。

これでFirebase Authenticationの準備は終了です。

iOSアプリの準備

以下の記述をinfo.plistに記述する必要があります。詳細はこちら。GoogleServices-Info.plistREVERSED_CLIENT_IDを抜き出して設定してください。

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <!-- TODO Replace this value: -->
            <!-- Copied from GoogleServices-Info.plist key REVERSED_CLIENT_ID -->
            <string>your_reversed_client_id</string>
        </array>
    </dict>
</array>

実装

前準備

前回作ったチャットアプリの続きから始めます。

pluginのインポート

firebase_authgoogle_sign_inを使用します。pubspec.yamlに以下の記述を追加します。(2018/08/27時点のバージョンを使用しています)

dependencies:
  firebase_auth: ^0.5.19
  google_sign_in: ^3.0.4

ソースコードにはこちらを記述してimportします、

import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';

Googleアカウントでログインできるボタンを用意

まず、クラス変数として、現在ログインしているユーザーを定義します。

  FirebaseUser _user;

ログインしなければ、ログインボタンを表示して、ログインしていればチャットを表示します。

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: new Text("Firebase Chat")),
      body: Container(
          child: _user == null ? _buildGoogleSignInButton() : _buildChatArea()),
    );
  }
  
  Widget _buildGoogleSignInButton() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Center(
            child: RaisedButton(
          child: Text("Google Sign In"),
          onPressed: () {
            _handleGoogleSignIn().then((user) {
              setState(() {
                _user = user;
              });
            }).catchError((error) {
              print(error);
            });
          },
        )),
      ],
    );
  }

_handleGoogleSignInメソッドで実際にログインするフローに入ります。(うーん、フォーマッターをかけているのにインデントがおかしい。。。)ログインに成功したら、クラス変数にユーザーをセットして再描画します。ログインできたら、チャットの画面が表示されます。

Google SignIn

こちらを参考にログイン処理を記述します。

  final _googleSignIn = new GoogleSignIn();
  final _auth = FirebaseAuth.instance;
  
  Future<FirebaseUser> _handleGoogleSignIn() async {
    GoogleSignInAccount googleUser = await _googleSignIn.signIn();
    GoogleSignInAuthentication googleAuth = await googleUser.authentication;
    FirebaseUser user = await _auth.signInWithGoogle(
      accessToken: googleAuth.accessToken,
      idToken: googleAuth.idToken,
    );
    print("signed in " + user.displayName);
    return user;
  }

チャットメッセージのデータ構造

チャットメッセージのデータ構造は以下のようにしました。メッセージ、ユーザー名、ユーザーのメールアドレス、ユーザーのアバターUrlを持ちます。

class ChatEntry {
  String key;
  DateTime dateTime;
  String message;
  String userName;
  String userEmail;
  String userImageUrl;

  ChatEntry(this.dateTime, this.message, FirebaseUser _user) {
    this.userName = _user.displayName;
    this.userEmail = _user.email;
    this.userImageUrl = _user.photoUrl;
  }

  ChatEntry.fromSnapShot(DataSnapshot snapshot)
      : key = snapshot.key,
        dateTime =
            new DateTime.fromMillisecondsSinceEpoch(snapshot.value["date"]),
        message = snapshot.value["message"],
        userName = snapshot.value["user_name"],
        userEmail = snapshot.value["user_email"],
        userImageUrl = snapshot.value["user_image_url"];

  toJson() {
    return {
      "date": dateTime.millisecondsSinceEpoch,
      "message": message,
      "user_name": userName,
      "user_email": userEmail,
      "user_image_url": userImageUrl,
    };
  }
}

チャット画面のレイアウト

少し長いですが、実装です。

  Widget _buildChatArea() {
    return Column(
      children: <Widget>[
        Expanded(
          child: ListView.builder(
            padding: const EdgeInsets.all(16.0),
            itemBuilder: (BuildContext context, int index) {
              return _buildRow(index);
            },
            itemCount: entries.length,
          ),
        ),
        Divider(
          height: 4.0,
        ),
        Container(
            decoration: BoxDecoration(color: Theme.of(context).cardColor),
            child: _buildInputArea())
      ],
    );
  }

  Widget _buildRow(int index) {
    ChatEntry entry = entries[index];
    return Container(
        margin: EdgeInsets.only(top: 8.0),
        child: _user.email == entry.userEmail
            ? _currentUserCommentRow(entry)
            : _otherUserCommentRow(entry));
  }

  Widget _currentUserCommentRow(ChatEntry entry) {
    return Row(children: <Widget>[
      Container(child: _avatarLayout(entry)),
      SizedBox(
        width: 16.0,
      ),
      new Expanded(child: _messageLayout(entry, CrossAxisAlignment.start)),
    ]);
  }

  Widget _otherUserCommentRow(ChatEntry entry) {
    return Row(children: <Widget>[
      new Expanded(child: _messageLayout(entry, CrossAxisAlignment.end)),
      SizedBox(
        width: 16.0,
      ),
      Container(child: _avatarLayout(entry)),
    ]);
  }

  Widget _messageLayout(ChatEntry entry, CrossAxisAlignment alignment) {
    return Column(
      crossAxisAlignment: alignment,
      children: <Widget>[
        Text(entry.userName,
            style: TextStyle(fontSize: 14.0, color: Colors.grey)),
        Text(entry.message)
      ],
    );
  }

  Widget _avatarLayout(ChatEntry entry) {
    return CircleAvatar(
      backgroundImage: NetworkImage(entry.userImageUrl),
    );
  }

メッセージのメールアドレスをみて、ログインしているユーザーと一緒であれば、自身が発信したメッセージとして左に寄せるレイアウトです。他者が発信したメッセージについては右側に寄せています。

最後に

ちょっと駆け足で解説しましたが、いかがだったでしょうか。実装は特にはまることもなく、順調に進みました。ブログを書いてる時間の方が圧倒的に長い。。。がんばろー。今回実装した、ソースコードはこちらに置いてあります。整理はしてないので汚いですが、誰かのお役に立てれば嬉しいです。

参考文献

  • Google APIs for Android: Authenticating Your Client
  • flutter/plugins
  • flutter-chat-app