iT邦幫忙

2025 iThome 鐵人賽

DAY 20
2

昨天完成了一重要的任務,將使用者的收藏資料遷移到了雲端Firestore,使用者的資料就會相對安全,且可以在任何登入相同帳號的裝置上存取... 嗯,最終是可以存取/images/emoticon/emoticon15.gif

讓我們想像一下:使用者在會議中用iPad收藏了一則靈感,會議結束後,他在捷運上拿出手機想再次回味,打開App,卻發現收藏頁面什麼都沒有!他必須先讓App重啟,才能觸發資料的重新抓取。這種「延遲感」破壞了雲端同步本應帶來的流暢體驗。

問題的根源在於,我們昨天用的是get() 方法,它就像是給資料庫拍了一張「快照」,只是一次性的拉取,而今天我們要將它升級為Firestore的另一核心能力 —— 即時更新(Realtime Updates)。將使用snapshots() 方法,把一次性的快照,變成一部資料庫的「現場直播影片」。任何風吹草動,都會即時地反映在我們App的UI上。

第一步:從get()到snapshots()的思維轉變

在動手之前,必須先理解這兩種方法的根本區別:

get() (拉取模式 Pull):

  • 你主動向資料庫發起一次請求:「嘿,現在的資料是什麼?」
  • 資料庫回傳給你那一瞬間的資料。
  • 之後資料庫再發生任何變化,你都一無所知,除非你再次發起 get() 請求。

snapshots() (推送模式 Push):

  • 你向資料庫建立一個持久的監聽連接:「嘿,請隨時告訴我資料的最新狀態。」
  • 資料庫會先回傳一次當前的完整資料。
  • 之後,只要資料庫中的那份資料有任何變動(新增、修改、刪除),資料庫就會主動將最新的全套資料「推送」給你。
  • snapshots()返回的是一個Stream(串流),正是這個串流,構成了所有即時App的心跳。

第二步:用StreamProvider改造收藏功能

在Riverpod的世界裡,要處理Stream的工具是StreamProvider,它會自動幫我們處理監聽、釋放、以及各種狀態(載入中、有資料、發生錯誤)。

再次對favorite_provider.dart進行重構,用StreamProvider來取代手動載入的邏輯。

// lib/features/favorite/favorite_provider.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:three_inspiration/data/models/insight_model.dart';

// 建立一個專門提供「收藏列表即時串流」的 Provider
final favoritesStreamProvider = StreamProvider<List<Insight>>((ref) {
  final user = FirebaseAuth.instance.currentUser;

  // 如果使用者未登入,返回一個空的串流
  if (user == null) {
    return Stream.value([]);
  }

  // 獲取 Firestore 的 Query
  final query = FirebaseFirestore.instance
      .collection('users')
      .doc(user.uid)
      .collection('favorites')
      .orderBy('favoritedAt', descending: true); // 我們可以順便加上排序

  // 監聽 snapshots() 串流
  final stream = query.snapshots();

  // 使用 .map 將 QuerySnapshot 轉換成我們需要的 List<Insight>
  return stream.map((snapshot) {
    return snapshot.docs.map((doc) => Insight.fromMap(doc.data())).toList();
  });
});

// PS: 昨天寫的 FavoriteNotifier (包含 add/remove 方法) 我們暫時可以保留,
// 因為它仍然負責「寫入」資料的邏輯。

第三步:將UI連接到StreamProvider

現在,我們的UI不再需要手動等待資料載入,而是直接監聽這個串流提供者即可。

打開lib/presentation/favorites_page.dart:

// lib/presentation/favorites_page.dart
class FavoritesPage extends ConsumerWidget {
  const FavoritesPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 1. 監聽 StreamProvider,它會返回一個 AsyncValue
    final favoritesAsyncValue = ref.watch(favoritesStreamProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('我的收藏'),
      ),
      // 2. 使用 .when 來優雅地處理三種狀態
      body: favoritesAsyncValue.when(
        // 載入中狀態
        loading: () => const Center(child: CircularProgressIndicator()),
        // 錯誤狀態
        error: (err, stack) => Center(child: Text('發生錯誤: $err')),
        // 成功獲取資料狀態
        data: (favorites) {
          if (favorites.isEmpty) {
            return const Center(
              child: Text('你還沒有收藏任何靈感喔!', style: TextStyle(fontSize: 18, color: Colors.grey)),
            );
          }
          // 直接使用返回的 favorites 列表建立 ListView
          return ListView.builder(
            itemCount: favorites.length,
            itemBuilder: (context, index) {
              final insight = favorites[index];
              // ... 返回你的 Card Widget
            },
          );
        },
      ),
    );
  }
}

第四步:實作「同步完成」提示

當另一台裝置的資料同步過來時,給使用者一個即時的反饋。可以利用ref.listen來監聽Provider的變化,並觸發像SnackBar這樣的UI事件。

在 FavoritesPage 的 build 方法內,ref.watch 的下方加入:

ref.listen<AsyncValue<List<Insight>>>(favoritesStreamProvider, (previous, next) {
  // 確保 next state 是有資料的狀態
  if (next is AsyncData) {
    // 為了簡化,我們可以在每次資料更新時都提示
    // 這裡可以加入更複雜的邏輯,判斷是否為「遠端」變更
    final aMomentAgo = DateTime.now().subtract(const Duration(seconds: 2));
    
    // 如果資料庫文件的時間戳很新,則認為是即時同步
    if (next.value.isNotEmpty && next.value.first.favoritedAt.isAfter(aMomentAgo)) {
       ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('✨ 收藏列表已即時同步!'),
          duration: Duration(seconds: 2),
        ),
      );
    }
  }
});

整合測試:

Step1. 體驗即時同步最好的方式,就是同時操作兩台裝置。
Step2. 在兩台裝置上執行App(例如一台實體手機,一台模擬器)。確保兩台裝置登入的是同一個使用者(對於匿名登入,這在測試時比較困難,可以先暫時實作一個簡單的信箱密碼登入來方便測試 )。
Step3. 在裝置A上,打開App並收藏一則新的靈感。
Step4. 立刻看向裝置B,會看到,幾乎在你手指離開螢幕的瞬間,裝置B上的收藏列表也自動出現了剛剛那則新的靈感,並彈出「已即時同步」的提示!
Step5. 反之,在裝置B上刪除一則收藏,裝置A上的對應項目也會瞬間消失。

這就是Firestore snapshots()讓資料在雲端流動,UI無縫響應。

明日預告:UI實作

目前App核心資料層已經算非常穩固且現代,現在,是時候為App梳妝打扮~明天將專注於UI優化與動畫點綴,為卡片滑動、按鈕點擊等互動加入細膩的動畫效果,讓App從不只「好用」,也很「好看」。


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


上一篇
30 天做一個極簡App:多端同步 - Firebase Firestore與Auth
下一篇
【30 天做一個極簡App】UI 實作:筆記區&設定頁
系列文
Mobile Dev|日更靈感來源 App:Flutter × LLM × n8n,每天只推 3 則!21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言