iT邦幫忙

2025 iThome 鐵人賽

DAY 16
2

昨天順利地為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

接下來是平台設定:

Android設定

不需要額外設定~workmanager v0.5.0 以上的版本已經能自動註冊,非常方便。

iOS設定

Step1. 開啟Xcode:在終端機中 cd ios 然後 open Runner.xcworkspace。

Step2. 啟用Background Modes:

  • 點擊左側的Runner 專案 -> 選擇Runner Target。
  • 切換到Signing & Capabilities分頁。
  • 點擊 + Capability 按鈕。
  • 在彈出的列表中,雙擊選擇Background Modes。
  • 在新增的Background Modes 區塊中,勾選 Background fetch。

Step3. 註冊任務識別碼:

  • 切換到Info分頁。
  • 在Custom iOS Target Properties中,找到 Permitted background task scheduler identifiers (如果沒有就手動新增一個Array類型的key)。
  • 點擊旁邊的 + 按鈕,新增一個 item,其值設定為你的 App 的Bundle ID,例如 com.example.threeInspiration。(你可以在 General 分頁找到你的 Bundle ID)。

第三步:實作背景任務

當作業系統在背景喚醒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


上一篇
30 天做一個極簡App:每日喚醒 FCM推播通知
下一篇
30 天做一個極簡App:原生分享功能
系列文
Mobile Dev|日更靈感來源 App:Flutter × LLM × n8n,每天只推 3 則!19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言