昨天順利地為App裝上了「擴音器」——FCM推播通知,讓它學會了如何主動呼喚使用者。當使用者收到「今日靈感已送達」的通知時,他們會滿懷期待地點開App。
但此時的體驗還有優化空間:使用者點開App後,App才開始從網路抓取最新的內容。如果網路稍有延遲,這份期待感就會被打斷。而我們能否更進一步,創造一種“心想事成”的魔法體驗?——讓使用者打開App的那一刻,最新的內容已經準備就緒。
答案是可以的!這需要借助背景作業 (Background Processing) 的力量~今天將使用workmanager這個強大的套件,來賦予我們的App在背景中「悄悄努力」的能力,接下,將設定一個每日定時任務,讓App即使在被關閉的狀態下,也能自動抓取最新的靈感並存入快取。
第一步:了解背景作業的運作與限制
在開始程式碼之前,先了解一件事:為了保護使用者的電量和裝置效能,行動作業系統(iOS 和 Android)對背景作業有著非常嚴格的限制。
Android:相對靈活。workmanager在Android上使用的是官方推薦的WorkManager API,它會以省電的方式來安排可延遲的背景任務。你可以設定一個大致的執行週期(例如 24 小時),系統會在這段時間內找一個合適的時機(例如裝置正在充電且連著 Wi-Fi)來執行它。
iOS:非常嚴格。iOS不允許App隨心所欲地在背景執行。workmanager在iOS上使用的是BGAppRefreshTask,你只能向系統「建議」一個最短執行間隔(約 15-20 分鐘),但最終何時執行、甚至是否執行,完全由iOS系統根據使用者習慣、電量等因素來決定。
workmanager的價值在於,它為我們抹平了這些複雜的平台差異,提供了一套統一的API。
第二步:安裝與平台設定
先安裝:
flutter pub add workmanager
接下來是平台設定:
不需要額外設定~workmanager v0.5.0 以上的版本已經能自動註冊,非常方便。
Step1. 開啟Xcode:在終端機中 cd ios 然後 open Runner.xcworkspace。
Step2. 啟用Background Modes:
Step3. 註冊任務識別碼:
第三步:實作背景任務
當作業系統在背景喚醒App時,App處於一個沒有UI的「無頭」狀態。因此需要提供一個頂層函式 (Top-level function) 作為背景執行的入口點。
將這段邏輯寫在lib/main.dart的頂部:
import 'package:workmanager/workmanager.dart';
// 引入我們需要的 Hive, model 和 repository
import 'package:hive_flutter/hive_flutter.dart';
import 'package:three_inspiration/data/models/insight_model.dart';
import 'package:path_provider/path_provider.dart' as path_provider;
// 假設你的 API 請求邏輯封裝在 Repository 中
import 'package:three_inspiration/data/repositories/insight_repository.dart';
// 1. 定義一個獨特的任務名稱
const fetchDailyTask = "com.example.threeInspiration.fetchDailyTask";
// 2. 建立頂層函式作為背景任務的入口
@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
switch (task) {
case fetchDailyTask:
try {
// 3. 在背景 isolate 中重新初始化服務
// 這是必要的,因為背景 isolate 和主 isolate 是隔離的
final appDocumentDir = await path_provider.getApplicationDocumentsDirectory();
await Hive.initFlutter(appDocumentDir.path);
Hive.registerAdapter(InsightAdapter());
await Hive.openBox<Insight>('insights_cache');
// 4. 執行你的核心邏輯:抓取資料並存入 Hive
final insightRepository = InsightRepository(); // 建立 Repository 實例
final freshInsights = await insightRepository.fetchInsights();
final box = Hive.box<Insight>('insights_cache');
await box.clear();
await box.putAll(Map.fromEntries(freshInsights.map((e) => MapEntry(e.id, e))));
debugPrint("背景任務執行成功:已抓取 ${freshInsights.length} 則新靈感並存入快取。");
return Future.value(true);
} catch (e) {
debugPrint("背景任務執行失敗: $e");
return Future.value(false);
}
default:
return Future.value(true);
}
});
}
關鍵:
@pragma('vm:entry-point'):這個註解是告訴Dart編譯器,即使在Tree Shaking (搖樹優化) 中,也要保留這個函式作為執行入口。
重新初始化:背景Isolate 是一個獨立的執行緒,它不共享主Isolate的記憶體。因此,所有需要的服務,如 Hive,都必須在callbackDispatcher內部重新初始化一次。
第四步:從App中註冊並排程任務
背景任務的程式碼已準備好了,現在需要在App啟動時,告訴workmanager何時去執行它。
修改lib/main.dart的main函式:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// ... Firebase 和 Hive 的初始化 ...
await Firebase.initializeApp(...);
await Hive.initFlutter(...);
// ...
// 初始化 Workmanager 並註冊背景任務
await Workmanager().initialize(
callbackDispatcher,
isInDebugMode: true, // 在開發模式下開啟日誌,方便除錯
);
// 註冊一個每日定時任務
await Workmanager().registerPeriodicTask(
fetchDailyTask, // 任務的唯一 ID
fetchDailyTask, // 傳遞給 callbackDispatcher 的任務名稱
frequency: const Duration(days: 1), // 頻率設置為一天
constraints: Constraints(
networkType: NetworkType.connected, // 限制在有網路時才執行
),
);
runApp(const ProviderScope(child: MyApp()));
}
現在App變得更加智慧和貼心,當使用者在早晨收到我們的FCM推播時,點開App的那一刻,最新的靈感已經無需等待,即刻呈現。
內容的獲取和呈現已經非常完善,那下一步呢?好的內容值得被分享:)明天將實作原生的分享功能,讓使用者可以輕鬆地將他們喜愛的靈感,透過任何通訊軟體或社群平台,分享給他們的朋友!
【哈囉你好:)感謝你的閱讀!其他我會常出沒的地方:Threads】