前回の記事で、Googleログイン機能の実装に成功しました。
しかし、「習慣データ」を登録する機能がありません。
今回は、Googleの「Cloud Firestore」を導入します。
これを使うと、ただデータが保存されるだけでなく、「スマホAで追加したら、スマホBの画面も表示する」という同期処理ができるようになります。
1. Webとモバイル、Firestoreの決定的な違い
以前Webアプリ編でもFirestoreを使いましたが、モバイル版(ネイティブSDK)にはWeb版にはない強力な機能が標準装備されています。
① オフラインでも動く(ローカルキャッシュ)
これが最大の違いです。
Web版ではネットが切れるとデータが見れなくなりますが、モバイル版は自動的にスマホ本体にデータをコピー(キャッシュ)して持っておいてくれます。
地下鉄などで電波が切れても、アプリは普通に使えます。そして電波が復活した瞬間に、裏側で勝手にサーバーと同期してくれます。これを自作するのは大変ですが、Firestoreなら簡単に導入できます。
② リアルタイム更新(Stream)
「データを取得する」という概念が変わります。
「これください」と頼むのではなく、「このデータを見張っておく(Subscribe)」という書き方をします。
誰かがデータを書き換えた瞬間、サーバーからスマホに通知が来て、画面が自動で再描画されます。
2. Androidアプリでの準備
設定ファイルは「そのままでOK」
前回 google-services.json を配置しましたよね?
実はあれ一つで、認証も、データベースも、解析もすべてつながります。追加の設定ファイルは不要です。
⚠️ 秘密鍵(Service Account)は絶対に使わない
ネット上の古い記事で「管理者SDK(秘密鍵JSON)」をアプリに入れる手順を見かけることがありますが、絶対にやってはいけません。
アプリの中に「合鍵」を入れるようなものです。
モバイルアプリでは、前回の google-services.json(公開前提の設定)を使い、セキュリティは後述する「セキュリティルール」で担保するのが鉄則です。
パッケージの追加
VS Codeのターミナルで、Firestore用のパッケージを追加します。
flutter pub add cloud_firestore
3. データ設計とセキュリティルール
コードを書く前に、Firebaseコンソール側で「箱」を用意します。
Step 1: データベースの作成
- Firebaseコンソールの「Firestore Database」を開き、「データベースの作成」を押します。
- ロケーションは
asia-northeast1(東京) を推奨します。 - セキュリティルールは「本番モードで開始」を選びます(最初は誰もアクセスできません)。
Step 2: ルールの設定(超重要)
「自分のデータは自分しか見れない」というルールを書きます。
「ルール」タブを開き、以下のように書き換えて「公開」を押してください。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// usersコレクションの中身は、本人のみ読み書きOK
match /users/{uid}/{document=**} {
allow read, write: if request.auth != null && request.auth.uid == uid;
}
}
}
これで、users/【自分のID】/habits/ というデータには、自分しかアクセスできなくなりました。
4. 【実装】リアルタイム習慣トラッカー
では、前回のコードを改造します。
ポイントは StreamBuilder です。これを使うと、データベースの変更を監視して、画面を自動更新できます。
lib/main.dart の HabitListScreen 部分を以下のように書き換えてください。
import 'package:cloud_firestore/cloud_firestore.dart'; // 追加
// (前半のMyAppやLoginScreenは前回のまま変更なし)
class HabitListScreen extends StatefulWidget {
const HabitListScreen({super.key});
@override
State<HabitListScreen> createState() => _HabitListScreenState();
}
class _HabitListScreenState extends State<HabitListScreen> {
// ログイン中のユーザーIDを取得
final String uid = FirebaseAuth.instance.currentUser!.uid;
// 追加ボタンの処理
void _addHabit() {
showDialog(
context: context,
builder: (context) {
String newHabit = '';
return AlertDialog(
title: const Text('新しい習慣を追加'),
content: TextField(
autofocus: true,
onChanged: (value) => newHabit = value,
),
actions: [
TextButton(
onPressed: () async {
if (newHabit.isNotEmpty) {
// ▼ Firestoreに追加!
await FirebaseFirestore.instance
.collection('users') // 親フォルダ
.doc(uid) // 自分のIDフォルダ
.collection('habits') // 習慣データフォルダ
.add({
'title': newHabit,
'createdAt': FieldValue.serverTimestamp(), // 時間順に並べる用
'isDone': false,
});
}
if (context.mounted) Navigator.pop(context);
},
child: const Text('追加'),
),
],
);
},
);
}
// 削除処理
void _deleteHabit(String docId) {
FirebaseFirestore.instance
.collection('users')
.doc(uid)
.collection('habits')
.doc(docId)
.delete();
}
// 完了トグル処理
void _toggleHabit(String docId, bool currentStatus) {
FirebaseFirestore.instance
.collection('users')
.doc(uid)
.collection('habits')
.doc(docId)
.update({'isDone': !currentStatus});
}
@override
Widget build(BuildContext context) {
final user = FirebaseAuth.instance.currentUser;
return Scaffold(
appBar: AppBar(
title: Text('${user?.displayName ?? "自分"}の習慣'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () => FirebaseAuth.instance.signOut(),
)
],
),
// ▼ ここが魔法のパーツ「StreamBuilder」
body: StreamBuilder<QuerySnapshot>(
// 監視するクエリ:自分のhabitsを作成日順で
stream: FirebaseFirestore.instance
.collection('users')
.doc(uid)
.collection('habits')
.orderBy('createdAt', descending: true)
.snapshots(),
builder: (context, snapshot) {
// 1. エラーが起きた時
if (snapshot.hasError) {
return const Center(child: Text('エラーが発生しました'));
}
// 2. 読み込み中(データがまだない時)
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
// 3. データ表示
final docs = snapshot.data!.docs;
if (docs.isEmpty) {
return const Center(child: Text('習慣を追加してみましょう!'));
}
return ListView.builder(
itemCount: docs.length,
itemBuilder: (context, index) {
final data = docs[index].data() as Map<String, dynamic>;
final docId = docs[index].id; // ドキュメントID(削除などに使う)
return Card(
margin: const EdgeInsets.all(8),
child: ListTile(
leading: Checkbox(
value: data['isDone'] ?? false,
onChanged: (val) => _toggleHabit(docId, data['isDone'] ?? false),
),
title: Text(data['title'] ?? ''),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.grey),
onPressed: () => _deleteHabit(docId),
),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _addHabit,
child: const Icon(Icons.add),
),
);
}
}
5. 動作確認と「感動」の瞬間
コードを保存して、アプリを再起動(デバッグ実行)してみましょう。
① データを追加する
右下の「+」ボタンで習慣を追加してみてください。
一見今までと変わりませんが、今度はFirebaseのコンソールを見てみてください。
「Firestore Database」の中に、あなたの入力したデータがリアルタイムで増えているはずです!
② コンソールからいじってみる
ここが一番面白いところです。
- スマホアプリを開いたままにします。
- PCのブラウザ(Firebaseコンソール)で、データを直接編集(例: タイトルを書き換える、削除する)してみてください。
- スマホの画面が、触ってもいないのに勝手に書き換わりましたよね?
これが「リアルタイム同期」です。
この機能のおかげで、チャットアプリや在庫管理アプリなどが簡単に作れるのです。
まとめ:バックエンドはもう要らない?
今回は、AndroidアプリとFirestoreを連携させました。
サーバーを一切構築せず、SQLも書かず、たったこれだけのコードで「認証付き・リアルタイムデータベース」が完成してしまいました。