[Flutter x Firebase] Cloud Firestoreと連携してカウントを保持する
やること
- 前回から作成しているソーシャルログイン機能つきのカウンターアプリとCloud Firestoreを連携して、カウントを保持できるカウンターアプリにする。
前回
- [Flutter x Firebase] カウンターアプリに認証機能を追加する
- [Flutter x Firebase] カウンターアプリに認証機能を追加する 〜自動ログイン〜
- [Flutter x Firebase] Crashlyticsと連携してクラッシュレポートを取得する
CrashlyticsはなくてもOK。
今回は認証情報を誰のカウントなのかわかるようにするために使用する。(ユーザーIDとかとカウントを紐付ける)
カウンターアプリのいけてないところ
- アプリ起動中でしか カウントを保持できない。
→Cloud Firestoreでログインユーザーごとにカウントを保持する
カウントを保持するだけだったら
SharedPreferences
やsqlite
などのDBでもできるが、今回はFirebaseのお勉強ということで。。。
成果物
動作イメージ
左側がFirebaseコンソールのCloud Firestoreの管理画面。
カウントを増やすと リアルタイムに反映されていることがわかる。
ソースコード
Cloud Firestoreを理解する
料金
個人&お試しで使う分には無料で使えそう。
データモデル
詳しくはこちら → Cloud Firestore データモデル | Firebase
-
Collection
とDocument
の概念が存在する。 -
Collection
の中に複数のDocument
が含まれる。 -
Document
にはjson形式のデータを格納することができる。
users: [ // Collection
user1: { // Document
name: {
first: "taro",
last: "tanaka"
},
age: 23,
},
user2: { // Document
name: {
first: "hanako",
last: "yamada",
},
age: 36,
},
・・・
]
Documentを追加する
詳しくはこちら → Cloud Firestore | FlutterFire
まずは、FirebaseFirestore.instance.collection(コレクション名)
でCollectionへのリファレンスを取得する。Collectionはこの時勝手にできるっぽい?
そして、リファレンス.add(データ)
でDocumentを追加する。
この場合、作成したDocumentには自動生成されたIDが割り振られる。
// Create a CollectionReference called users that references the firestore collection
CollectionReference users = FirebaseFirestore.instance.collection('users');
Future<void> addUser() {
// Call the user's CollectionReference to add a new user
return users
.add({
'full_name': fullName, // John Doe
'company': company, // Stokes and Sons
'age': age // 42
})
.then((value) => print("User Added"))
.catchError((error) => print("Failed to add user: $error"));
}
今回はユーザーを特定するためのIDとカウントを紐付けたいので、自分でIDを設定してDocumentを作成したい。
自分でIDを指定したい場合は、doc(ドキュメントID)
とset(データ)
を用いる。
CollectionReference users = FirebaseFirestore.instance.collection('users');
Future<void> addUser() {
return users
.doc('ABC123') // <= IDを指定
.set({
'full_name': "Mary Jane",
'age': 18
})
.then((value) => print("User Added"))
.catchError((error) => print("Failed to add user: $error"));
}
Documentを読み込む
リファレンス.doc(ドキュメントID).get()
で取ってこれる。
return FutureBuilder<DocumentSnapshot>(
future: users.doc(documentId).get(),
builder:
(BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {
if (snapshot.hasError) {
return Text("Something went wrong");
}
if (snapshot.connectionState == ConnectionState.done) {
Map<String, dynamic> data = snapshot.data.data();
return Text("Full Name: ${data['full_name']} ${data['last_name']}");
}
return Text("loading");
},
);
リアルタイムに変更を適用したい時は以下のようにリファレンス.snapshots()
とStreamBuilder
を使う。
class UserInformation extends StatelessWidget {
Widget build(BuildContext context) {
CollectionReference users = FirebaseFirestore.instance.collection('users');
return StreamBuilder<QuerySnapshot>(
stream: users.snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Text("Loading");
}
return new ListView(
children: snapshot.data.documents.map((DocumentSnapshot document) {
return new ListTile(
title: new Text(document.data()['full_name']),
subtitle: new Text(document.data()['company']),
);
}).toList(),
);
},
);
}
}
Documentを更新する
doc(ドキュメントID)
でDocumentを指定し、update(データ)
でデータを更新する。
CollectionReference users = FirebaseFirestore.instance.collection('users');
Future<void> updateUser() {
return users
.doc('ABC123')
.update({'company': 'Stokes and Sons'})
.then((value) => print("User Updated"))
.catchError((error) => print("Failed to update user: $error"));
}
オフラインDB
Firestoreでは、オフライン時に書き込み・読み込みがあった時のためのローカルデータベースが備わっており、オンラインになった時に自動的に同期を取ってくれる。すごい。
デフォルトでオンになっているので、使いたくない場合以外は何もしなくてもOK。
その他
今回は使わないが、指定した条件にマッチするDocumentだけ取ってくるクエリ機能もある。
手順
パッケージのインストール
dependencies:
cloud_firestore: ^0.14.3
FirebaseコンソールからCloud Firestoreのデータベースを作成する
選択肢
- 本番用 or テスト用 → テスト用を選択
- リージョン(一度決めたら変えられない) → asia-northeast1 (東京)を選択
実装時のポイント
- DocumentIDとして使うために、ユーザーのユニークなIDを取得する。
- ログイン成功時に、そのユーザーに対するDocumentを作成する。
- カウンター画面を描画するときに、カウントをFirestoreに保持しているカウントにセットする。
- +ボタン押下時に+1したカウントでDocumentを更新する。
ログイン中のユーザーのユニークなID
ログイン中のユーザーを表すクラスにはfirebase_auth.dart
で定義されているUser
を用いているが、
User
にはuid
(The user's unique ID.)が定義されているので、それをDocumentIDに使うことにする。
今回はProviderパターンで状態管理(AuthModel
)をしており、カウンター画面が定義されているmain.dart
からAuthModel
のuser.uid
を取得するには、context.select
やcontext.read
を使う。
final String userId = context.select((AuthModel auth) => auth.user.uid);
final String userId = context.read<AuthModel>().user.uid;
カウンターDocumentを新規に作成する
今回はログイン成功時に上記で取得したuid
を用いてDocumentを作成することにした。
lib/helpers/firestore_helper.dart
を作成し、createCounter
を作成。
class FirestoreHelper {
FirestoreHelper._internal();
static final FirestoreHelper _instance = FirestoreHelper._internal();
CollectionReference _counters =
FirebaseFirestore.instance.collection('counters');
static FirestoreHelper get instance => _instance;
Future<bool> createCounter(String userId) async {
try {
await _counters.doc(userId).set({'count': 0});
} catch (error) {
print(error);
return false;
}
return true;
}
}
そして、models/auth_model.dart
のlogin
関数にcreateCounter
を差し込む。
class AuthModel extends ChangeNotifier {
~~
Future<bool> login(ButtonType type) async {
~~
try {
UserCredential _userCredential = await _signInWithGoogle();
/* ↓ 追加 ↓ */
await FirestoreHelper.instance.createCounter(_userCredential.user.uid);
_user = _userCredential.user;
notifyListeners();
return true;
} catch (error) {
print(error);
return false;
}
}
}
この状態でアプリを起動しログインに成功すると、Cloud Firestoreの管理画面のデータタブで、counters
というCollectionと、長い文字列が名前のDocument
({count:0})が生成されていることが確認できる。
カウンターDocumentがないときにのみ作成するようにする
上記の処理を追加することによって、ログイン成功時にDocumentを作成することができるようになったが、ログインするたびにDocumentを作成してしまうと{count:0}
で上書きされてしまうためカウントが0に戻ってしまう。
そのため、createCounter
を指定されたIDのDocumentがなかったら新規に作成する
というように変更する。
DocumentIDが同じときに上書きするか・しないかみたいな設定が見当たらなかったため。
class FirestoreHelper {
~~
Future<bool> _hasCounter(String userId) async {
final DocumentSnapshot snapshot = await fetchCounter(userId);
if (snapshot == null || // 指定したIDに該当するDocumentがない
snapshot.data() == null || // 該当するDocumentはあるけどデータがまだない
snapshot.data()['count'] == null) { // 該当するDocumentはあるけど、指定したFieldがない
return false;
}
return true;
}
Future<bool> createCounter(String userId) async {
try {
// 既にカウンターがある場合は何もしないで終了
if (await _hasCounter(userId)) {
return true;
}
await _counters.doc(userId).set({'count': 0});
} catch (error) {
print(error);
return false;
}
return true;
}
~~
}
カウンター画面描画時にカウンターDocumentを取得する
上記でDocumentの作成を完了したので、今度はそれをカウンター画面で参照できるようにしたい。
カウンターアプリでは、カウンター画面の描画時に初期値をセットしたり、+ボタンを押すたびに値を増やしたりなど、カウンターDocumentにアクセスする機会が多いので、Stream
とStreamBuilder
を使ってリアルタイムに値を反映させることにする。(get
とFutureBuilder
でもできなくはない。)
まずは FirestoreHelper
クラスに 指定されたIDのDocumentのStream
を取得する関数を追加する。
Stream<DocumentSnapshot> getStream(String userId) => _counters.doc(userId).snapshots();
次に、カウントを表示している部分(Text
ウィジェット)をStreamBuilder
でラップした関数_buildCounter()
を新たに作成する。
元々
_MyHomePageState
クラスにあった変数_counter
は_currentCount
にリネームしました。
Widget _buildCounter() {
final String userId = context.select((AuthModel auth) => auth.user?.uid);
return StreamBuilder<DocumentSnapshot>(
stream: _firestoreHelper.getStream(userId),
builder:
(BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {
if (snapshot.hasError) {
print(snapshot.error);
return Text('エラーが発生しました。');
}
if (snapshot.hasData) {
final Map<String, dynamic> data = snapshot.data.data();
if (data != null && data['count'] != null) {
_currentCount = data['count'];
}
// 元々あったTextウィジェット
return Text(
'$_currentCount',
style: Theme.of(context).textTheme.headline4,
);
}
return CircularProgressIndicator();
},
);
}
カウンターDocumentを更新する
最後に、+ボタンを押したときに現在のカウントに+1してからDocumentを更新する。
以下のようにFirestoreHelper
クラスにupdateCounter()
関数を追加する。
class FirestoreHelper {
~~
Future<bool> updateCounter(String userId, int count) async {
try {
await _counters.doc(userId).update({'count': count});
} catch (error) {
print(error);
return false;
}
return true;
}
}
そして、カウンター画面の_incrementCounter()
関数を以下のように書き換える。
Future<void> _incrementCounter() async {
_currentCount++;
final String userId = context.read<AuthModel>().user.uid;
await _firestoreHelper.updateCounter(userId, _currentCount);
}
(おまけ)カウントリセットボタンを追加する
ようやくカウントを保持できるカウンターアプリに進化したが、カウントをリセットできた方がより便利である。
ということで、AppBar.actions
にもう1つボタンを足して、リセットボタンを追加する。
class _MyHomePageState extends State<MyHomePage> {
~~
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
centerTitle: true,
actions: [
IconButton(
icon: Icon(Icons.delete),
onPressed: () => _firestoreHelper.updateCounter(
context.read<AuthModel>().user.uid,
0,
),
),
IconButton(
icon: Icon(Icons.logout),
onPressed: _logout,
),
],
),
~~
以上。長くなってしまった。
Discussion