はじめに
前回:Flutterやってみたよ part1 (導入編)に続いてFlutterとFirebaseの連携をやってみようと思います。
Firebaseは無料で使える機能が充実していますので、個人的には積極的に使っていきたいです〜
(どこぞやに買収とかされないことを祈っております)
今回は@nunnally_engr_0114さんのFirebaseとFlutterでアプリ開発してみる【其ノ二:Flutter&Dart篇】
を参考にさせてもらいました。勉強になりました、本当にありがとうございますm(_ _)m
目的
- FirebaseAuthを利用してサインイン・サインアウト機能を有した画面を作る
- まだアーキテクチャは導入しない(次回にやりたい)
今回で画面を作ってみて画面遷移方法・状態管理方法など学べたらいいなと思います。
そこまでいったらアークテクチャの導入をやってみようと思います。
画面構成
まずは画面を準備しようと思います。構成はシンプルに
- スプラッシュスクリーン
- サインイン
- サインアップ
- ホーム
画面実装
flutterのライフサイクルとかの基礎は他の記事を参考にということで割愛します。
以下が参考になりました。
参考
公式サイト
Flutter の Widget ツリーの裏側で起こっていること
Stateful Widget のパフォーマンスを考慮した正しい扱い方
パッケージの導入
使用するパッケージを導入します。
必要になったら都度追加でOKです。今回は作ってから記事を書いてますのでまとめて
pubspec.yaml
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
firebase_core: ^0.3.0
firebase_auth: ^0.9.0
firebase_messaging: ^4.0.0
cloud_firestore: ^0.9.0
cloud_functions: ^0.1.0
progress_dialog: ^1.2.0
intl: ^0.15.7
画像の追加
画像を追加する場合もpubspec.yamlに宣言を追加して見えるようにする必要があります。
今回はassetsの直下にsplash.pngをおきました。
flutter:
assets:
- assets/splash.png
スプラッシュスクリーン
// ここがコンストラクタみたいなもの。引数を追加して初期化するところです。
// 今回はauthを引数に渡して初期化しています
class SplashScreen extends StatefulWidget {
SplashScreen({Key key, this.auth}) : super(key: key);
final BaseAuth auth;
// ここが画面を構成するところ。privateな_SplashScreenStateを呼び出す形にしてます
@override
State<StatefulWidget> createState() => _SplashScreenState();
}
// これが画面を構成するクラス
class _SplashScreenState extends State<SplashScreen> {
AuthStatus _authStatus = AuthStatus.notSignedIn;
// 初期処理で認証してタイマーを起動
@override
void initState() {
super.initState();
// Firebase 認証
widget.auth.currentUser().then((userId) {
_authStatus = userId != null ? AuthStatus.signedIn : AuthStatus.notSignedIn;
});
_startTimer();
}
// buildが画面を構成しているところです。
// 今回は画面にスプラッシュ画像をセットしてます
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Image(image: AssetImage('assets/splash.png')),
),
),
);
}
// 3秒後に認証ずみならホーム、未認証ならサインインに遷移します
_startTimer() async {
Timer(Duration(seconds: 3), () {
switch (_authStatus) {
case AuthStatus.notSignedIn:
Navigator.pushReplacement(context, MaterialPageRoute(
builder: (context) => new SignInScreen(title: 'SignIn', auth: widget.auth)
));
break;
case AuthStatus.signedUp:
Navigator.push(context, MaterialPageRoute(
builder: (context) => new SignUpScreen(title: 'SignUp', auth: widget.auth)
));
break;
case AuthStatus.signedIn:
Navigator.pushReplacement(context, MaterialPageRoute(
builder: (context) => new HomeScreen(title: 'Home', auth: widget.auth)
));
break;
}
});
}
}
サインイン
サインインのページです。
class SignInScreen extends StatefulWidget {
SignInScreen({Key key, this.title, this.auth}) : super(key: key);
final String title; // タイトル
final BaseAuth auth; // 認証状態
@override
_SignInScreenState createState() => _SignInScreenState();
}
class _SignInScreenState extends State<SignInScreen> {
static final formKey = new GlobalKey<FormState>();
User _user = new User();
String _authHint = '';
// これは画面のバリデーションチェックでエラーになっているか判定します。
bool validateAndSave() {
final form = formKey.currentState;
// widgetにはバリデーション機能が備わっているので画面上のバリデーションをform単位で検証することができます。
if (form.validate()) {
// OKだったらsaveすることでバリデーションの状態を保存できます
form.save();
return true;
}
return false;
}
// サインインボタンをタップした時のイベント
void validateAndSubmit(BuildContext context) async {
setState(() {
_authHint = '';
});
if (validateAndSave()) {
// グルグルを使用(package:progress_dialogを使用)
final ProgressDialog progress = new ProgressDialog(context);
try {
// グルグルを表示。
progress.show();
// awaitで実行が終わるまで待ちます。
await widget.auth.signIn(_user.email, _user.password);
// グルグル閉じてから遷移する
if (progress.isShowing()) {
progress.hide();
progress.dismiss();
}
// ホームに遷移
Navigator.pushReplacement(context, MaterialPageRoute(
builder: (context) => new HomeScreen(title: 'Home', auth: widget.auth)
));
} catch (e) {
// なんかエラーがでたら画面に表示します
setState(() {
_authHint = 'サインインでエラーが発生しました。\n\n${e.toString()}';
});
print(e);
} finally {
// グルグルを閉じる
if (progress.isShowing()) {
progress.hide();
progress.dismiss();
}
}
}
}
// サインアップボタンのイベントです
void signUpSubmit() async {
setState(() {
_authHint = '';
});
// サインアップに遷移
Navigator.push(context, MaterialPageRoute(
builder: (context) => new SignUpScreen(title: 'SignUp', auth: widget.auth)
));
}
// ヒントエリアのコンポーネント生成メソッド
Widget hintText() {
return new Container(
padding: const EdgeInsets.all(32.0),
child: new Text(
_authHint,
key: new Key('hint'),
style: new TextStyle(fontSize: 18.0, color: Colors.grey),
textAlign: TextAlign.center
),
);
}
// paddingのコンポーネント生成メソッド
Widget padded({Widget child}) {
return new Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: child,
);
}
// 入力テキストエリアのコンポーネント生成メソッド
List<Widget> usernameAndPassword() {
return [
padded(child: new TextFormField(
key: new Key('email'),
decoration: new InputDecoration(labelText: 'Email'),
autocorrect: false,
validator: (val) => val.isEmpty ? 'emailを入力してください' : null,
onSaved: (val) => _user.email = val,
)),
padded(child: new TextFormField(
key: new Key('password'),
decoration: new InputDecoration(labelText: 'Password'),
obscureText: true,
autocorrect: false,
validator: (val) => val.isEmpty ? 'パスワードを入力してください' : null,
onSaved: (val) => _user.password = val,
)),
];
}
// ボタンエリアのコンポーネント生成メソッド
List<Widget> submitWidgets(BuildContext context) {
return [
new PrimaryButton(
key: new Key('signIn'),
text: 'サインイン',
height: 44.0,
onPressed: () => validateAndSubmit(context)
),
new FlatButton(
key: new Key('need-account'),
textColor: Colors.green,
child: new Text(
"初めて利用する方\n(サインアップ)",
textAlign: TextAlign.center
),
onPressed: signUpSubmit
),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Login Screen'),
),
body: new Center(
child: new Form(
child: new SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// それっぽく画像を表示
const SizedBox(
height: 200.0,
width: 200.0,
child: Image(
image: AssetImage('assets/splash.png'),
fit: BoxFit.fill,
),
),
hintText(),
const SizedBox(height: 10.0),
new Center(
child: new Form(
key: formKey,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: usernameAndPassword() + submitWidgets(context),
)
)
),
],
),
),
),
),
);
}
}
サインアップ
class SignUpScreen extends StatefulWidget {
SignUpScreen({Key key, this.title, this.auth}) : super(key: key);
final String title;
final BaseAuth auth;
@override
_SignUpState createState() => new _SignUpState();
}
class _SignUpState extends State<SignUpScreen> {
static final formKey = new GlobalKey<FormState>();
User _user = new User();
String _authHint = '';
bool isValidate() {
final form = formKey.currentState;
if (form.validate()) {
form.save();
return true;
}
return false;
}
// サインインタップ時のメソッド
void onPressSignIn() async {
setState(() {
_authHint = '';
});
// 前の画面に戻る
Navigator.pop(context);
}
// サインアップップ時のメソッド
void onPressSignUp() async {
setState(() {
_authHint = '';
});
if (isValidate()) {
// グルグルを使用(package:progress_dialogを使用)
final ProgressDialog progress = new ProgressDialog(context);
try {
// グルグルを表示
progress.show();
// FirebaseAuthでユーザ作成
await widget.auth.createUser(_user.email, _user.password, _user.displayName);
// 成功したらそのままサインイン
await widget.auth.signIn(_user.email, _user.password);
// グルグルと閉じてから遷移
if (progress.isShowing()) {
progress.hide();
progress.dismiss();
}
// ホームへ遷移
Navigator.pushReplacement(context, MaterialPageRoute(
builder: (context) => new HomeScreen(title: 'Home', auth: widget.auth)
));
} catch (e) {
// なんかエラー出たらヒントに表示
setState(() {
_authHint = 'サインインでエラーが発生しました。\n\n${e.toString()}';
});
print(e);
} finally {
// グルグルを閉じる
if (progress.isShowing()) {
progress.hide();
progress.dismiss();
}
}
}
}
// 入力エリアコンポーネント生成メソッド
List<Widget> inputWidgets() {
return [
padded(child: new TextFormField(
key: new Key('displayName'),
decoration: new InputDecoration(labelText: 'Name'),
autocorrect: false,
validator: (val) => val.isEmpty ? '表示名を入力してください' : null,
onSaved: (val) => _user.displayName = val,
)),
padded(child: new TextFormField(
key: new Key('email'),
decoration: new InputDecoration(labelText: 'Email'),
autocorrect: false,
validator: (val) => val.isEmpty ? 'emailを入力してください' : null,
onSaved: (val) => _user.email = val,
)),
padded(child: new TextFormField(
key: new Key('password'),
decoration: new InputDecoration(labelText: 'Password'),
obscureText: true,
autocorrect: false,
validator: (val) => val.isEmpty ? 'パスワードを入力してください' : null,
onSaved: (val) => _user.password = val,
)),
];
}
// ボタンコンポーネント生成メソッド
List<Widget> submitWidgets() {
return [
new PrimaryButton(
key: new Key('register'),
text: 'サインアップ',
height: 44.0,
onPressed: onPressSignUp
),
new FlatButton(
key: new Key('login'),
textColor: Colors.green,
child: new Text(
"既にアカウントをお持ちの方\n(サインイン)",
textAlign: TextAlign.center
),
onPressed: onPressSignIn
),
];
}
// ヒントエリアのコンポーネント生成メソッド
Widget hintText() {
return new Container(
//height: 80.0,
padding: const EdgeInsets.all(32.0),
child: new Text(
_authHint,
key: new Key('hint'),
style: new TextStyle(fontSize: 18.0, color: Colors.grey),
textAlign: TextAlign.center)
);
}
// paddingのコンポーネント生成メソッド
Widget padded({Widget child}) {
return new Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: child,
);
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
backgroundColor: Colors.grey[300],
body: new SingleChildScrollView(child: new Container(
padding: const EdgeInsets.all(16.0),
child: new Column(
children: [
hintText(),
new Card(
child: new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Container(
padding: const EdgeInsets.all(16.0),
child: new Form(
key: formKey,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: inputWidgets() + submitWidgets(),
)
)
),
])
),
]
)
))
);
}
ホーム
ホームはドロワーだけ作って中身はありません、、、すみませんmm
class HomeScreen extends StatefulWidget {
HomeScreen({Key key, this.title, this.auth, this.onSignOut}) : super(key: key);
final String title;
final BaseAuth auth;
final VoidCallback onSignOut;
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// サインアウトを実行してサインインへ遷移
void onSignOut() {
try {
widget.auth.signOut();
Navigator.pushReplacement(context, MaterialPageRoute(
builder: (context) => new SignInScreen(title: 'SignIn', auth: widget.auth)
));
} catch (e) {
// トーストでエラー表示してみた
Fluttertoast.showToast(
msg: 'サインアウトでエラーが発生しました。\n\n${e.toString()}',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
timeInSecForIos: 1,
backgroundColor: Colors.red,
textColor: Colors.white,
fontSize: 12.0
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Text('Home'),
),
drawer: new Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
DrawerHeader(
child: Text('メニュー'),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/splash.png'),
fit: BoxFit.fill,
alignment: Alignment.center,
),
),
),
ListTile(
title: Text('サインアウト'),
onTap: onSignOut,
),
],
),
),
);
}
}
Firebaseのapi作成
今回はemail、パスワードを使って認証するようになってます。
メッセージも使いたいなと思ってたのでとりあえずトークンも紐づけてます。
ここで見慣れない Future というものが出てきましたので軽く説明します。
FutureとはDartで非同期処理を書く時の標準のクラスです。
swiftやkotlinでの非同期処理(Rx)やったことあるならすんなり理解できると思います。
アーキテクチャを導入すればRxdartを使用することになると思います。
参考
Future<T> class
Dartでの非同期処理(then vs async/await)
abstract class BaseAuth {
Future<String> signIn(String email, String password);
Future<void> signOut();
Future<String> createUser(String email, String password, String displayName);
Future<String> updateUser(String email, String password, String displayName);
Future<String> currentUser();
}
enum AuthStatus {
notSignedIn,
signedIn,
signedUp
}
class Auth implements BaseAuth {
final FirebaseAuth _firebaseAuth;
final FirebaseMessaging _firebaseMessaging;
final BaseUtil _util = new Util();
Auth({FirebaseAuth firebaseAuth, FirebaseMessaging firebaseMessaging})
: _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance,
_firebaseMessaging = firebaseMessaging ?? new FirebaseMessaging()
;
Future<String> signIn(String email, String password) async {
FirebaseUser user = await _firebaseAuth.signInWithEmailAndPassword(email: email, password: password);
// deviceToken取得
String token = await _firebaseMessaging.getToken();
// Usersテーブル更新
var db = Firestore.instance;
await db.collection("users").document(user.uid).updateData({
"deviceToken": token,
"signInAt": _util.nowDatetimeString()
});
return user.uid;
}
Future<void> signOut() async {
return _firebaseAuth.signOut();
}
Future<String> createUser(String email, String password, String displayName) async {
FirebaseUser user = await _firebaseAuth.createUserWithEmailAndPassword(email: email, password: password);
// Firebase UserInfo 更新
UserUpdateInfo info = new UserUpdateInfo();
info.displayName = displayName; // 表示名前
user.updateProfile(info);
// deviceToken取得
String _token = await _firebaseMessaging.getToken();
// Usersテーブル作成
var db = Firestore.instance;
await db.collection("users").document(user.uid).setData({
"deviceToken": _token,
"displayName": displayName,
"createdAt": _util.nowDatetimeString(),
"updatedAt": _util.nowDatetimeString(),
"deletedAt": ''
});
return user.uid;
}
Future<String> updateUser(String email, String password, String displayName) async {
FirebaseUser user = await _firebaseAuth.createUserWithEmailAndPassword(email: email, password: password);
// Firebase UserInfo 更新
UserUpdateInfo info = new UserUpdateInfo();
info.displayName = displayName; // 表示名前
user.updateProfile(info);
// deviceToken取得
String _token = await _firebaseMessaging.getToken();
// Usersテーブル作成
var db = Firestore.instance;
await db.collection("users").document(user.uid).updateData({
"deviceToken": _token,
"displayName": displayName,
"updatedAt": _util.nowDatetimeString()
});
return user.uid;
}
Future<String> currentUser() async {
FirebaseUser user = await _firebaseAuth.currentUser();
return user != null ? user.uid : null;
}
}
ソース
現時点(2019/12/26)でのソース公開します。随時更新していきます❗️
flutter_firebase_sample
終わりに
なんとなくflutterでアプリが作れそうな気がしてきましたwww
ただこのままじゃお仕事で使えないので次回はアーキテクチャ調査編です。
ではまた👋
参考
大変勉強になりました!ありがとうございました🙇♂️