🔰基礎技術

【Flutter実践】Androidアプリのデータをクラウドに保存する|Cloud FirestoreでToDoリストを作る手順

前回の記事で、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: データベースの作成

  1. Firebaseコンソールの「Firestore Database」を開き、「データベースの作成」を押します。
  2. ロケーションは asia-northeast1 (東京) を推奨します。
  3. セキュリティルールは「本番モードで開始」を選びます(最初は誰もアクセスできません)。

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.dartHabitListScreen 部分を以下のように書き換えてください。

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」の中に、あなたの入力したデータがリアルタイムで増えているはずです!

② コンソールからいじってみる

ここが一番面白いところです。

  1. スマホアプリを開いたままにします。
  2. PCのブラウザ(Firebaseコンソール)で、データを直接編集(例: タイトルを書き換える、削除する)してみてください。
  3. スマホの画面が、触ってもいないのに勝手に書き換わりましたよね?

これが「リアルタイム同期」です。
この機能のおかげで、チャットアプリや在庫管理アプリなどが簡単に作れるのです。

まとめ:バックエンドはもう要らない?

今回は、AndroidアプリとFirestoreを連携させました。
サーバーを一切構築せず、SQLも書かず、たったこれだけのコードで「認証付き・リアルタイムデータベース」が完成してしまいました。

-🔰基礎技術