iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0

哈囉鐵人們!現在此App是一個非常出色的單機版應用~它功能豐富、體驗流暢,甚至還能離線使用。但它有一個天生的「地域限制」——那就是所有的使用者資料,如那些特別收藏的靈感,都被鎖在一台裝置的Hive資料庫裡。

如果使用者換了新手機怎麼辦?如果他想在手機和iPad 看到同樣的收藏列表怎麼辦?目前唯一的答案是:辦不到!

今天將引入Firebase的兩大王牌功能,將使用者的核心資料遷移到雲端,打造真正的多端同步:

  • Firebase Authentication:為每一位使用者提供一個獨一無二的雲端身份,而且是在他們完全無感的情況下完成。
  • Cloud Firestore:一個強大、可擴展的NoSQL雲端資料庫,它將成為使用者資料的新家。

第一步:安裝Firebase套件

之前已經安裝了 firebase_core,現在需要加入Auth和Firestore的套件:
flutter pub add firebase_auth cloud_firestore

第二步:實作Firebase,匿名登入

要讓資料跟著「使用者」走,首先得知道「使用者」是誰。但強制使用者註冊帳號會增加使用門檻,最好的方式是「匿名登入」。Firebase Auth會在背景為使用者建立一個永久的、獨一無二的帳號,而使用者完全不需要進行任何操作。

Step1. 在Firebase控制台啟用匿名登入

  • 前往你的 Firebase 專案 -> Authentication -> Sign-in method 分頁。
  • 點擊「匿名」,將其啟用並儲存。

Step2. 在App啟動時自動登入
確保使用者一打開App就擁有一個身份,修改lib/main.dart的main函式:

// 在 main.dart 頂部引入
import 'package:firebase_auth/firebase_auth.dart';

Future<void> main() async {
  // ... 其他初始化 ...
  await Firebase.initializeApp(...);

  // 檢查當前是否有使用者登入,若無,則進行匿名登入
  if (FirebaseAuth.instance.currentUser == null) {
    try {
      await FirebaseAuth.instance.signInAnonymously();
      debugPrint("已成功匿名登入!");
    } catch (e) {
      debugPrint("匿名登入失敗: $e");
    }
  }

  // ... Hive, Workmanager 初始化 ...
  runApp(...);
}

現在,每個開App的使用者(無論新舊),都會被賦予一個獨一無二的uid (User ID),這個uid就是我們接下來在資料庫中區分不同使用者資料的關鍵鑰匙。

第三步:設定 Cloud Firestore 與資料結構

Step1. 在 Firebase 控制台建立資料庫

  • 前往你的Firebase專案 -> Firestore Database。
  • 點擊「建立資料庫」。
  • 選擇「以測試模式啟動」。這會允許我們在開發階段自由讀寫資料庫,30天後規則會失效(到時需要設定更安全的規則)。
  • 選擇離你使用者最近的地區,然後啟用。

Step2. 規劃使用者獨立的資料空間
這是今天最核心的概念。為了不讓A使用者的收藏被B使用者看到,必須以uid來隔離資料。規劃結構如下:

users (集合 Collection)
└── Hq5k...xZ2 (文件 Document, 此處為使用者的 uid)
    ├── profile (欄位 Field): { displayName: "Guest", createdAt: ... }
    │
    └── favorites (子集合 Sub-collection)
        │
        └── abc...123 (文件 Document, 此處為靈感的 id)
            ├── summary: "靈感的摘要..."
            ├── tags: ["Flutter", "UI"]
            ├── sourceUrl: "https://..."
            └── favoritedAt: Nov 20, 2025 at 10:00:00 AM UTC+8
  • 有一個頂層的users集合。
  • users 集合中的每一個文件,都以使用者的uid命名。
  • 在每個使用者文件底下,我們再建立一個名為favorites的子集合,專門存放這位使用者的收藏。

第四步:將「收藏」邏輯從Hive遷移至 Firestore

現在,要對Day 14建立的FavoriteNotifier進行一次大手術,讓它從讀寫本地Hive轉為讀寫雲端Firestore。
打開lib/features/favorite/favorite_provider.dart:

import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
// ... 其他 import

class FavoriteNotifier extends StateNotifier<List<Insight>> {
  FavoriteNotifier() : super([]) {
    // 當使用者登入狀態改變時,自動加載他們的收藏
    FirebaseAuth.instance.authStateChanges().listen((user) {
      if (user != null) {
        _loadFavorites(user.uid);
      } else {
        // 如果使用者登出,清空收藏列表
        state = [];
      }
    });
  }

  // 從 Firestore 載入收藏
  Future<void> _loadFavorites(String userId) async {
    try {
      final snapshot = await FirebaseFirestore.instance
          .collection('users')
          .doc(userId)
          .collection('favorites')
          .get();
      
      final favorites = snapshot.docs.map((doc) => Insight.fromMap(doc.data())).toList();
      state = favorites;
    } catch (e) {
      debugPrint("載入 Firestore 收藏失敗: $e");
    }
  }

  // 新增收藏
  Future<void> addFavorite(Insight insight) async {
    final userId = FirebaseAuth.instance.currentUser?.uid;
    if (userId == null) return; // 如果未登入,則不執行任何操作

    if (!state.any((item) => item.id == insight.id)) {
      // 寫入 Firestore
      await FirebaseFirestore.instance
          .collection('users')
          .doc(userId)
          .collection('favorites')
          .doc(insight.id)
          .set(insight.toMap()); // 將 Insight 物件轉為 Map

      // 更新本地狀態
      state = [...state, insight];
    }
  }

  // 移除收藏
  Future<void> removeFavorite(String insightId) async {
    final userId = FirebaseAuth.instance.currentUser?.uid;
    if (userId == null) return;

    // 從 Firestore 刪除
    await FirebaseFirestore.instance
        .collection('users')
        .doc(userId)
        .collection('favorites')
        .doc(insightId)
        .delete();

    // 更新本地狀態
    state = state.where((insight) => insight.id != insightId).toList();
  }
}

// ... Provider 維持不變

小提示:你需要在Insight模型中加入toMap()和fromMap()方法,以便在Dart物件和Firestore的Map格式之間轉換。

明日預告:即時更新 (Realtime Updates)

使用者的資料終於安全地存放在了雲端。但目前的體驗還有一點延遲:如果你在一台手機上收藏了某個靈感,需要重啟另一台手機上的App才能看到同步。如何讓這個同步過程變成即時的呢?

明天,將繼續探索Firestore方便的另一特性:即時更新 (Realtime Updates),將學會如何監聽雲端資料的變化,並讓App的UI瞬間、自動地做出反應!


【哈囉你好:)感謝你的閱讀!其他我會常出沒的地方:Threads


上一篇
30 天做一個極簡App:從點擊通知到 App的Deep Linking
系列文
Mobile Dev|日更靈感來源 App:Flutter × LLM × n8n,每天只推 3 則!19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言