昨天我們安裝並初始化了Hive,定義了資料的「Box」,還教會了Hive如何去認識我們的Insight模型。今天將扮演工程師的角色,把Riverpod的狀態管理系統和Hive的本地儲存系統這兩條「管道」連接起來。
將修改Provider,讓它在App啟動時從Hive讀取資料,在使用者操作時將資料寫回Hive。今天App將徹底告別「失憶症」,無論是重啟App還是處於離線狀態,使用者的茲料都將安全無虞。
第一步:讓「我的收藏」擁有永久記憶
我們先從最直接、最有成就感的「收藏」功能開始。改造 FavoriteNotifier,讓它從記憶體模式切換到硬碟模式。
打開lib/features/favorite/favorite_provider.dart,我們將進行三處修改:
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart'; // 1. 引入 hive_flutter
import 'package:three_inspiration/data/models/insight_model.dart';
class FavoriteNotifier extends StateNotifier<List<Insight>> {
// 獲取 Hive Box 的實例
final Box<Insight> _box = Hive.box<Insight>('favorites');
FavoriteNotifier() : super([]) {
// 2. 在建構時,直接從 Box 讀取所有值來初始化 state
state = _box.values.toList();
}
void addFavorite(Insight insight) {
if (!state.any((item) => item.id == insight.id)) {
// 3. 在更新 state 的同時,使用 put 方法寫入 Hive
// 我們使用 insight 的 id 作為 key,確保唯一性
_box.put(insight.id, insight);
state = [...state, insight];
}
}
void removeFavorite(String insightId) {
// 4. 在更新 state 的同時,使用 delete 方法從 Hive 移除
_box.delete(insightId);
state = state.where((insight) => insight.id != insightId).toList();
}
}
final favoriteProvider = StateNotifierProvider<FavoriteNotifier, List<Insight>>((ref) {
return FavoriteNotifier();
});
立即驗證!
現在,重新編譯並執行你的App。去收藏幾則靈感,然後徹底關閉 App(從系統後台滑掉),再重新打開它。會發現——你收藏的那些靈感,依然靜靜地躺在收藏頁面裡~它們還在!
第二步:快取每日靈感,實現離線瀏覽
搞定了收藏列表,接下來我們來處理更複雜一些的每日靈感快取。我們希望達到的效果是:
這就是經典的 "Cache then Network" (先快取,後網路) 策略。讓我們來改造 InsightNotifier。
打開lib/features/insight/insight_provider.dart:
import 'package:flutter/foundation.dart'; // 用於 debugPrint
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:three_inspiration/data/models/insight_model.dart';
class InsightNotifier extends StateNotifier<List<Insight>> {
final Box<Insight> _box = Hive.box<Insight>('insights_cache');
InsightNotifier() : super([]) {
// 1. 立即從快取加載資料,讓 UI 快速顯示
state = _box.values.toList();
debugPrint('從 Hive 快取中加載了 ${state.length} 則靈感。');
// 2. 在背景執行網路請求
_fetchFromApi();
}
Future<void> _fetchFromApi() async {
try {
// 模擬網路請求,未來會換成真實的 Repository
debugPrint('正在從 API 獲取新靈感...');
await Future.delayed(const Duration(seconds: 2)); // 模擬網路延遲
final freshInsights = const [
Insight(id: '1', summary: '靈感一:[來自網路] 文章驅動開發是一種絕佳實踐。', tags: ['開發流程', '敏捷']),
Insight(id: '2', summary: '靈感二:[來自網路] 在設計 UI 時,給予足夠的「留白」。', tags: ['UI/UX', '設計原則']),
Insight(id: '3', summary: '靈感三:[來自網路] 使用 n8n 這類視覺化工作流工具。', tags: ['n8n', '自動化']),
];
debugPrint('成功獲取 ${freshInsights.length} 則新靈感!');
// 3. 成功後,更新 Hive 快取
await _box.clear(); // 清空舊快取
// 使用 putAll 批量寫入,key 為 insight.id
await _box.putAll(Map.fromEntries(freshInsights.map((e) => MapEntry(e.id, e))));
// 4. 更新 UI 狀態
state = freshInsights;
} catch (e) {
// 5. 如果發生錯誤(例如沒有網路),則不做任何事
// UI 會繼續顯示從快取中讀取的舊資料
debugPrint('獲取新靈感失敗:$e。將繼續使用快取資料。');
}
}
}
final insightNotifierProvider = StateNotifierProvider<InsightNotifier, List<Insight>>((ref) {
return InsightNotifier();
});
離線測試!
再次執行App。你會看到主頁先是快速顯示了上次的資料(我在假資料中加入了 [來自網路] 來區分),過了2秒後,UI會刷新並顯示新的資料。
接下來,打開手機的飛航模式,然後重啟App。會發現,即使沒有網路,App依然能正常顯示主頁的靈感卡片!控制台會印出錯誤日誌,但App對使用者來說是完全可用的,這就是離線快取的功用。
一個好的 App 不僅要被動地提供服務,還應該能主動地與使用者溝通。如果每天都有新的靈感,我們該如何通知使用者「嘿,今天的靈感更新了!」呢?
明天將進入下一個全新的領域:推播通知。將整合 Firebase Cloud Messaging (FCM),讓我們的App即使在被關閉時,也能向使用者發送充滿吸引力的通知!
【哈囉你好:)感謝你的閱讀!其他我會常出沒的地方:Threads】