株式会社diffeasyでエンジニア修行僧のナナリー(@Nunnally_Engr)です。
この記事は、diffeasy Advent Calendar 2018 25日目の記事です。
■ 今回お話すること
- Flutterについて
- Flutter環境構築について
- FlutterとFirebaseとの連携周りについて(主にアプリ登録、認証:Authentication、データベース:CloudFirestore)
■ 前提
- 今回作ったアプリは個人的に作った請求アプリです。
- まだ開発途中段階なので、変更箇所については記事も更新しますが、gitリポジトリのソースと記事とで違う場合があります。(gitのリポジトリを信じてください(汗))
- 今回は『FirebaseとFlutterでアプリ開発してみる【其ノ一:CloudFunctionsでの処理篇】』でお話したFlutter側の内容となってます。
■ アプリとしての流れ
-
サインインの画面からメールアドレス、パスワードを使ってFirebaseにサインインし、CloudFirestoreのユーザテーブル(users)にデバイストークン(deviceToken)を更新する。
-
サインアップの画面からメールアドレス、パスワードを使ってFirebaseに新規登録し、CloudFirestoreのユーザテーブル(users)にデバイストークン(deviceToken)を登録する。
-
請求画面からCloudFunctionsを利用して、GCPのMySQLに請求データを登録する。
今回は上記赤枠の部分のお話をしてみたいと思います(๑•̀ㅁ•́๑)✧
■ 今回の作成したプログラムの全貌
- gitHub:pay_wish_app
※ ちょこちょこ修正入ります、あしからず…
■ 今回の環境
> 開発端末
MacOS Mojave 10.14.1
SCV36(android-arm64)
> Flutter | Dart
Flutter 1.0.0 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 5391447fae (6 days ago) • 2018-11-29 19:41:26 -0800
Engine • revision 7375a0f414
Tools • Dart 2.1.0 (build 2.1.0-dev.9.4 f9ebf21297)
google_sign_in: ^3.2.4
firebase_auth: ^0.6.6
firebase_messaging: ^2.1.0
cloud_firestore: ^0.8.2+3
cloud_functions: ^0.0.5
> Firebase
Firebase CLI: 6.1.2
Node.js 8
firebase-admin: ~6.0.0
firebase-functions: ^2.1.0
mysql: ^2.16.0
> GCP:Cloud SQL
MySQL5.7 第2世代
> Android Studio
Android Studio 3.2.1
Build #AI-181.5540.7.32.5056338, built on October 9, 2018
JRE: 1.8.0_152-release-1136-b06 x86_64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
macOS 10.14.2
■ 開発する前準備
1. Firebase プロジェクト作成
- 作成手順は割愛します。当記事で使用しているプロジェクトは「pay-wish」としてお話を進めていきます。
2. Firebase Authenticationの設定
3. Firebase Cloud Firestoreの初期設定
- 以下のテーブルを定義しています。(詳細は割愛)
users
├ {$uid}
├ deviceToken
4. DartのSDKインストール
- FlutterではDartという言語で動いているので、brewコマンドを使ってインストールしておきます。
$ brew tap dart-lang/dart
$ brew install dart --with-content-shell --with-dartium
5. FlutterのSDKインストール
- 以下のサイトからSDKのZipダウンロードして、任意のディレクトリに配置してください。導入手順も以下のサイトに記載されています。
⇒ Get the Flutter SDK for Mac
6. Android Studioの環境構築
-
以下のサイトがわかりやすく記載されてますので、『4. Android環境を構築 > 4.1. Android Studioをインストール』の箇所を参考にインストール&設定してみてください。
⇒ Flutter開発環境構築(Mac編) -
【参考】新規インストール直後の場合、以下のような「Complete Installationダイアログ」が表示されます。これは旧バージョンの設定引継ぎを行うかどうかを問い合わせているのですが、引き継ぐ環境などありませんので「Do not import settings」を選択してください。
7. Android仮想端末設定
- AndroidStudioを起動したら、右上の方にアイコンがあるので選択します。
- 「Create Virtual Device」を選択します。
- 端末を選ぶ画面が表示されるので、好きな端末を選択し、「Next」ボタンを押してください。(私はGalaxyが好きなのでその端末を選択しました(笑))
- システムイメージで"Pie"を選択し、「Next」ボタンをおしてください。
- 特にこだわりがない人はそのまま「Finish」ボタンを押して設定を終えてください。
- 先程設定した端末が表示されます。
8. VSCodeプラグインインストール(任意)
- VsCodeで開発したので、私は以下のプラグインをインストールしました。
- Dart
- Flutter
- Flutter Widget Snippets
■ Flutter環境構築
1. プロジェクトを作成する
2. プロジェクト名設定
3. プロジェクトディレクトリ選択
4. ライブラリ追加
- 以下のライブラリをyamlファイルへ追加します。必ず最新のバージョンを追加してください。
- firebase_auth
- firebase_messaging
- cloud_firestore
- intl
dependencies:
google_sign_in: ^3.2.4
firebase_auth: ^0.6.6
firebase_messaging: ^2.1.0
cloud_firestore: ^0.8.2+3
cloud_functions: ^0.0.5
intl: ^0.15.7
- ライブラリを追加したら、
flutter packages get
が自動で実行されて.packages
が更新されてしまうので、更新後以下のパスを追加しましょう。
login:lib/
src:lib/src
5. 仮想端末を選択
6. Flutter起動する
- 一旦、設定ができているかどうか確認します。以下のコマンドを実行しましょう。
$ flutter doctor
- 実行結果です。(今回はAndroidのみで開発するのでiOSでの設定はしていません。取り急ぎ、iOSのエラーは放置します。)
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, v1.0.0, on Mac OS X 10.14.2 18C54, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK 28.0.3)
[✗] iOS toolchain - develop for iOS devices
✗ Xcode installation is incomplete; a full installation is necessary for iOS development.
Download at: https://developer.apple.com/xcode/download/
Or install Xcode via the App Store.
Once installed, run:
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
✗ libimobiledevice and ideviceinstaller are not installed. To install with Brew, run:
brew update
brew install --HEAD usbmuxd
brew link usbmuxd
brew install --HEAD libimobiledevice
brew install ideviceinstaller
✗ ios-deploy not installed. To install with Brew:
brew install ios-deploy
✗ CocoaPods not installed.
CocoaPods is used to retrieve the iOS platform side's plugin code that responds to your plugin usage on the Dart side.
Without resolving iOS dependencies with CocoaPods, plugins will not work on iOS.
For more info, see https://flutter.io/platform-plugins
To install:
brew install cocoapods
pod setup
[✓] Android Studio (version 3.2)
[✓] VS Code (version 1.30.1)
[✓] Connected device (1 available)
! Doctor found issues in 1 category.
- 以下のコマンドでサンプルアプリが起動するかを確認しましょう。カウントアップアプリが表示されたらFlutterの設定は完了です。
$ flutter run
6. ディレクトリ構造の確認
- 今回説明するソースの構造は以下の通りとなっています。
- (*)のソースは、今回私が作成したファイルと、Firebaseの設定ファイルになります。(ポイントの部分だけ後ほど説明します)
pay_wish_app/
├ android/
├ build.gradle
├ app/
├ build.gradle
├ google-services.json(*)
├ src/
├ main/
├ AndroidManifest.xml
├ lib/
├ src/(*)
├ auth.dart
├ table/
├ users.dart
├ claim.dart(*)
├ dashboard.dart(*)
├ main.dart
├ primary_button.dart(*)
├ sign_in.dart(*)
├ sign_up.dart(*)
├ .packages
├ pubspec.yaml
■ Firebaseとの連携
1. Firebaseのプロジェクト設定
-
Android パッケージ名に、
pay_wish_app/android/app/build.gradle
に設定されている、applicationIdを設定しましょう。
android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.paywishapp"
}
}
buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.0.1'
}
}
dependencies {
implementation 'com.google.firebase:firebase-core:16.0.1'
}
apply plugin: 'com.google.gms.google-services'
2. Flutterを実行して連携する
$ flutter run
■ 実装ファイル説明: 認証(Authentication)
- 今回Firebase Authenticationの処理を、abstract classに設定しました。
import 'dart:async';
// Firebase ↓ (01) 必要なpackageをimport
import 'package:firebase_auth/firebase_auth.dart';
import 'package:src/table/users.dart';
abstract class BaseAuth {
Future<String> signIn(String email, String password);
Future<String> createUser(String email, String password, String displayName, String photoUrl);
Future<String> currentUser();
Future<void> signOut();
}
class Auth implements BaseAuth {
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
final BaseUsers _users = new Users();
Future<String> signIn(String email, String password) async {
// Firebase Authentication サインイン ↓ (02) サインインの処理
FirebaseUser user = await _firebaseAuth.signInWithEmailAndPassword(email: email, password: password);
// Usersテーブル更新
await _users.update(user.uid);
return user.uid;
}
Future<String> createUser(String email, String password, String displayName, String photoUrl) async {
// Firebase Authentication 登録 & サインイン ↓ (03) 登録 & サインイン
FirebaseUser user = await _firebaseAuth.createUserWithEmailAndPassword(email: email, password: password);
// Firebase UserInfo 更新 ↓ (04) AuthenticationのuserInfo更新処理
UserUpdateInfo info = new UserUpdateInfo();
info.displayName = displayName; // 表示名前
info.photoUrl = photoUrl; // 画像URL
user.updateProfile(info);
// Usersテーブル作成
await _users.create(user.uid);
return user.uid;
}
Future<String> currentUser() async {
FirebaseUser user = await _firebaseAuth.currentUser();
return user != null ? user.uid : null;
}
Future<void> signOut() async {
return _firebaseAuth.signOut();
}
}
(01) 必要なpackageをimportします。
(02) Future<String> signIn(String email, String password)
でメールアドレスとパスワードを使ってサインインの処理をさせます。firebase.authのsignInWithEmailAndPasswordメソッドに、メールアドレスとパスワードを渡すだけです。
(03) Future<String> createUser(String email, String password, String displayName, String photoUrl)
でメールアドレスとパスワードを使ってユーザの登録&サインインをさせます。firebase.authのcreateUserWithEmailAndPasswordメソッドに、メールアドレスとパスワードを渡すだけです。
(04)Authenticationが用意している、userInfoの表示名と画像のURLも更新できるように設定しています。
■ 実装ファイル説明: Dartの状態管理について
-
今回、画面としては①サインイン画面、②サインアップ画面、③ダッシュボード、④請求画面と4画面用意してます。表示する画面や認証状態をDartの状態管理方法であるStateを使って制御します。状態管理や画面の遷移に関しては、scoped model・redux・BLoCといくつかあるそうなのですが、今回はmain.dartでRootPageをStatefulWidgetとして利用し、main.dartで状態を管理する方法でやってみます。
-
では実際に画面制御や状態を制御しているmain.dartの説明をします。
// flutter
import 'package:flutter/material.dart';
// Firebase
import 'package:firebase_messaging/firebase_messaging.dart';
// Page
import 'package:src/auth.dart';
import 'package:login/sign_in.dart';
import 'package:login/sign_up.dart';
import 'package:login/dashboard.dart';
import 'package:login/claim.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Firebase Login',
theme: new ThemeData(
primarySwatch: Colors.orange,
),
home: new RootPage(auth: new Auth())
);
}
}
class RootPage extends StatefulWidget {
RootPage({Key key, this.auth}) : super(key: key);
final BaseAuth auth;
@override
State<StatefulWidget> createState() => new _RootPageState();
}
// 状態定義 ↓ (01) 認証状態と画面情報定義
enum AuthStatus {
notSignedIn,
signedIn,
signedUp
}
// カレントページ ↓ (01) 認証状態と画面情報定義
enum CurrentPage {
dashboard,
claim,
other
}
class _RootPageState extends State<RootPage> {
AuthStatus authStatus = AuthStatus.notSignedIn;
CurrentPage currentPage = CurrentPage.other;
initState() {
final FirebaseMessaging _firebaseMessaging = new FirebaseMessaging();
super.initState();
// Firebase 認証 ↓ (02) カレントユーザ情報取得
widget.auth.currentUser().then((userId) {
setState(() {
authStatus = userId != null ? AuthStatus.signedIn : AuthStatus.notSignedIn;
});
});
// Firebase FCM
_firebaseMessaging.configure(
onMessage: (Map<String, dynamic> message) async {
print("onMessage: $message");
_buildDialog(context, "onMessage:請求が届いたようだ(๑•̀ㅁ•́๑)✧");
},
onLaunch: (Map<String, dynamic> message) async {
print("onLaunch: $message");
_buildDialog(context, "onLaunch:請求が届いたようだ(๑•̀ㅁ•́๑)✧");
},
onResume: (Map<String, dynamic> message) async {
print("onResume: $message");
_buildDialog(context, "onResume:請求が届いたようだ(๑•̀ㅁ•́๑)✧");
},
);
// Push通知の許可
_firebaseMessaging.requestNotificationPermissions(
const IosNotificationSettings(sound: true, badge: true, alert: true));
// Push通知の許可・設定(iOS)
_firebaseMessaging.onIosSettingsRegistered
.listen((IosNotificationSettings settings) {
print("Settings registered: $settings");
});
}
// ダイアログを表示するメソッド
void _buildDialog(BuildContext context, String message) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return new AlertDialog(
content: new Text("Message: $message"),
actions: <Widget>[
new FlatButton(
child: const Text('CLOSE'),
onPressed: () {
Navigator.pop(context, false);
},
),
new FlatButton(
child: const Text('SHOW'),
onPressed: () {
Navigator.pop(context, true);
},
),
],
);
}
);
}
// 認証状態更新 ↓ (03)Stateを更新する処理
void _updateAuthStatus(AuthStatus status) {
setState(() {
authStatus = status;
});
}
// カレントページを更新 ↓ (03)Stateを更新する処理
void _updateCurrentPage(CurrentPage page) {
setState(() {
currentPage = page;
});
}
@override
Widget build(BuildContext context) {
// 認証状態に応じて表示する画面を分ける ↓ (04)画面制御処理
switch (authStatus) {
case AuthStatus.notSignedIn:
print('■ サインイン');
// サインインページ
return new SignIn(
title: 'Flutter Firebase SignIn',
auth: widget.auth,
onSignIn: () => _updateAuthStatus(AuthStatus.signedIn),
onSignUp: () => _updateAuthStatus(AuthStatus.signedUp),
);
case AuthStatus.signedIn:
switch (currentPage) {
case CurrentPage.claim:
print('■ 請求画面');
// 請求画面
return new Claim(
auth: widget.auth,
onSignOut: () => _updateAuthStatus(AuthStatus.notSignedIn),
currentPageDashboardSet: () => _updateCurrentPage(CurrentPage.dashboard)
);
default:
print('■ ダッシュボード');
// ダッシュボードページ
return new Dashboard(
auth: widget.auth,
onSignOut: () => _updateAuthStatus(AuthStatus.notSignedIn),
currentPageClaimSet: () => _updateCurrentPage(CurrentPage.claim)
);
}
break;
case AuthStatus.signedUp:
print('■ サインアップ');
// 新規登録ページ
return new SignUp(
title: 'Flutter Firebase SignUp',
auth: widget.auth,
onSignOut: () => _updateAuthStatus(AuthStatus.notSignedIn),
onSignIn: () => _updateAuthStatus(AuthStatus.signedIn)
);
}
}
}
(01)認証状態と、表示している画面の情報を定義しています。
(02)initStateで、Firebaseからカレントユーザの情報を取得し、Stateに保持させます。
(03)Stateを更新する処理を記述します。この処理を、画面遷移時にVoidCallbackで渡して遷移先でこのStateを更新することによって、画面を切り替えてます。
(04)画面制御を行っている部分です。まず①認証していない・②認証済み・③新規登録というAuthStatus Stateで条件分岐し、②認証済みの場合は、①ダッシュボード・②請求画面のCurrentPage Stateで条件分岐をし画面を切り替えてます。
- 呼び出された側の処理今回はダッシュボード側の処理dashboard.dartを見ていきます。
import 'package:flutter/material.dart';
import 'package:src/auth.dart';
import 'package:cloud_functions/cloud_functions.dart';
class Dashboard extends StatelessWidget {
Dashboard({this.auth, this.onSignOut, this.currentPageClaimSet});
final BaseAuth auth;
final VoidCallback onSignOut; // ← (01) main.dartで渡した処理をVoidCallbackで受け取る
final VoidCallback currentPageClaimSet;
@override
Widget build(BuildContext context) {
void _signOut() async {
try {
await auth.signOut();
onSignOut(); // ← (02) state更新処理を走らせ、画面遷移させる
} catch (e) {
print(e);
}
}
// 登録ボタン
void onPressdClaimCreate() async {
// ページセット
currentPageClaimSet();
}
// 取得ボタン
void onPressdClaimSelect() async {
print('>>> Click:onCallClaimsSelect');
final dynamic resp = await CloudFunctions.instance.call(
functionName: 'onCallClaimsSelect'
);
print(resp);
}
return new Scaffold(
appBar: new AppBar(
title: new Text('ダッシュボード(*´ω`*)'),
actions: <Widget>[
new FlatButton(
onPressed: _signOut, // ← (03)画面のヘッダーにある「サインアウト」を押した時の処理
child: new Text('サインアウト', style: new TextStyle(fontSize: 17.0, color: Colors.white))
)
],
),
body: Column(children: <Widget>[
Padding(
padding: EdgeInsets.all(20.0),
child: RaisedButton(
child: const Text('取得'),
color: Theme.of(context).accentColor,
elevation: 4.0,
splashColor: Colors.blueGrey,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
),
onPressed: onPressdClaimSelect,
),
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: onPressdClaimCreate,
label: Text("請求する"),
icon: Icon(Icons.add),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
}
(01)main.dartで渡したState更新処理をVoidCallbackにて受け取ります。
(02)サインアウト時に、VoidCallbackとして定義したState更新処理を実行することで、呼び出し元画面のStateが更新され、今の画面を破棄しサインインの画面を表示しています。
(03)画面のヘッダーにある「サインアウト」を押した時の処理で、onPressedで_signOut()
が動くように指定します。
■ 実装ファイル説明: データ操作(CloudFirestore)
- CloudFirestoreを使った登録、更新処理はusers.dartに記述しています。
import 'dart:async';
import 'package:intl/intl.dart';
// Firebase
import 'package:cloud_firestore/cloud_firestore.dart'; // ← (01) cloud_firestoreのpackageをインポート
import 'package:firebase_messaging/firebase_messaging.dart'; // ← (02) firebase_messagingのpackageをインポート
abstract class BaseUsers {
Future<void> create(String uid);
Future<void> update(String uid);
}
class Users implements BaseUsers {
final FirebaseMessaging _firebaseMessaging = new FirebaseMessaging();
Future<void> create(String uid) async {
String _token = await _firebaseMessaging.getToken(); // ← (03) FCMトークン取得
var db = Firestore.instance;
// ↓ (04) CloudFirestore.usersに登録
await db.collection("users").document(uid).setData({
"deviceToken": _token,
"createdAt": DateFormat("y/m/d H:mm", "en_US").format(new DateTime.now()),
"updatedAt": DateFormat("y/m/d H:mm", "en_US").format(new DateTime.now()),
"deletedAt": ''
});
}
Future<void> update(String uid) async {
String _token = await _firebaseMessaging.getToken(); // ← (03) FCMトークン取得
var db = Firestore.instance;
// ↓ (05) CloudFirestore.usersに更新
await db.collection("users").document(uid).updateData({
"deviceToken": _token,
"updatedAt": DateFormat("y/m/d H:mm", "en_US").format(new DateTime.now())
});
}
}
(01)cloud_firestoreのpackageをインポートします。
(02)今回、端末の情報としてデバイストークンを取得したいので、firebase_messagingのpackageもインポートしてます。
(03)FirebaseMessagingクラスのgetToken()を利用して、FCMのトークン情報を取得します。
(04)Firestoreクラスをインスタンス化し、setDataを利用して、データをCloudFirestore.usersテーブルに登録していきます。
(05)CloudFirestore.usersテーブルを更新する時は(04)と同じ様にデータをセットして、updateDataを利用して更新します。
■ アプリを動かしてみる
- サインイン ⇒ サインアップ ⇒ ダッシュボード
- サインイン ⇒ ダッシュボード
■ あとがき
- 画面遷移周りは正直苦労しました・・・。まだ理解していない部分もありますが・・・。
- 画面遷移と状態管理って結構ペアなイメージがあるので、簡単なアプリで知識を深めようと思います。
- ただ、Firebaesとの連携はすぐにできたのでアプリ開発を初めてするにはいいかもしれません。
■ 参考リンク
-
当記事を書くにあたって何度もみたページです。お世話になりましたm(_ _)m
-
【Firebase公式ドキュメント】firebase.auth.Auth
-
【Firebase公式ドキュメント】Firebase と Flutter を使ってみる
-
Dartの状態管理についての記事