Background Tasks #

Menjalankan kode di latar belakang (background execution) saat aplikasi tidak berada di foreground merupakan salah satu aspek paling menantang dalam pengembangan aplikasi seluler. Sistem operasi modern—baik Android maupun iOS—menerapkan aturan yang sangat ketat untuk menghemat daya baterai, mengurangi konsumsi RAM, dan menjaga performa sistem secara keseluruhan. Jika sebuah aplikasi mencoba mengonsumsi daya komputasi atau memori di luar batas yang wajar saat berada di latar belakang, sistem operasi tidak akan ragu untuk menghentikan paksa (kill) proses tersebut secara diam-diam.

Sebagai developer Flutter, kita harus memahami bahwa model eksekusi Dart yang berbasis single-thread menggunakan Isolate membawa dampak langsung pada bagaimana tugas latar belakang dirancang. Ketika aplikasi masuk ke background, kita tidak bisa sekadar menjalankan fungsi asinkron biasa dan berharap fungsi tersebut terus berjalan selamanya. Kita memerlukan pendekatan arsitektural yang matang, memahami batasan platform asli, dan memilih alat yang tepat untuk kebutuhan spesifik aplikasi kita.

Arsitektur Konkurensi dan Batasan Eksekusi Latar Belakang #

Ketika aplikasi Flutter kita ditutup oleh pengguna atau diminimalisasi ke latar belakang, siklus hidup aplikasi masuk ke dalam keadaan ditangguhkan (suspended). Pada kondisi ini, sistem operasi mematikan atau membekukan thread utama rendering aplikasi.

Tantangan utama dalam Flutter adalah arsitektur Isolate. Setiap Isolate Dart memiliki ruang memori heap dan event loop-nya sendiri yang terisolasi. Ketika kita menjalankan background task di bawah koordinasi sistem operasi (misalnya melalui penjadwal native), sistem akan memicu pembuatan Isolate baru yang mandiri (sering disebut headless isolate). Karena isolate latar belakang ini terpisah sepenuhnya dari Main Isolate aplikasi kita:

  • Kita tidak memiliki akses ke variabel global atau instansi singleton yang diinisialisasi di Main Isolate (seperti database lokal terbukanya, token auth tersimpan di memori RAM, atau status Provider/Riverpod).
  • Kita tidak memiliki BuildContext karena tidak ada rendering UI yang terjadi di latar belakang.
  • Setiap dependensi (seperti API client, SharedPreferences, atau modul enkripsi) harus diinisialisasi ulang dari awal secara mandiri di dalam konteks isolate latar belakang tersebut.

Komparasi Tiga Pola Background Task #

Untuk menangani berbagai kebutuhan eksekusi kode di luar thread UI utama, kita dapat mengelompokkan solusi ke dalam tiga pola utama yang didukung oleh ekosistem Flutter:

Fitur / KarakteristikPola 1: IsolatePola 2: WorkmanagerPola 3: Background Service
Tujuan UtamaMencegah UI membeku akibat kalkulasi CPU yang berat saat aplikasi terbuka.Sinkronisasi data berkala atau pengiriman data terjadwal saat aplikasi tertutup.Menjalankan tugas berkelanjutan yang tidak boleh terputus (GPS, audio stream).
Daya HidupMati saat aplikasi ditutup oleh pengguna atau di-kill OS.Dijamin berjalan oleh OS meskipun aplikasi ditutup (menggunakan penjadwal native).Terus berjalan di latar belakang selama service aktif.
Status UIAplikasi harus dalam keadaan aktif (foreground).Berjalan mandiri tanpa UI (headless execution).Berjalan di background, tetapi menampilkan indikasi ke pengguna (Android).
Batasan WaktuTidak ada batasan waktu spesifik selama aplikasi tetap terbuka.iOS: maksimal $\approx$ 30 detik. Android: maksimal $\approx$ 10 menit.Berjalan tanpa batas waktu selama sistem operasi mengizinkan.
Kebutuhan IzinTidak memerlukan izin khusus.Memerlukan izin background fetch dan background processing di Xcode.Android: FOREGROUND_SERVICE permission. iOS: Kategori background khusus.

Berikut adalah visualisasi arsitektural dari ketiga pola background task tersebut dan bagaimana sistem operasi memicu eksekusinya:

flowchart TD
    subgraph IsolatePattern["1. Pola Isolate (Foreground)"]
        MainIsolate["Main Isolate (UI Thread)"] <-->|"SendPort / ReceivePort"| ChildIsolate["Child Isolate (CPU Tasks)"]
        NoteIsolate["Berjalan di dalam proses aplikasi.<br/>Mati saat aplikasi ditutup."]
    end

    subgraph WorkmanagerPattern["2. Pola Workmanager (Terjadwal)"]
        OS_Scheduler["Sistem OS (Android JobScheduler / iOS BGTaskScheduler)"]
        OS_Scheduler -->|"Triggers (Headless)"| EntryPoint["@pragma('vm:entry-point')<br/>callbackDispatcher"]
        EntryPoint -->|"Spawn Headless Isolate"| BackgroundTask["Task Executor"]
        NoteWM["Berjalan secara periodik/sekali.<br/>OS menentukan waktu mulai."]
    end

    subgraph ServicePattern["3. Pola Background Service (Terus-menerus)"]
        ForegroundService["Foreground Service (Android)"] <-->|"Notifikasi Terlihat"| UserUI["Pengguna UI"]
        ForegroundService -->|"Spawn Long-lived Isolate"| ActiveWorker["Active Worker (GPS/Audio)"]
        NoteService["Terus berjalan di background.<br/>iOS dibatasi pada kategori tertentu."]
    end

Pola 1: Isolate (Komputasi Latar Belakang di Foreground) #

Pola pertama ini digunakan ketika aplikasi kita dalam keadaan aktif di layar pengguna, tetapi kita perlu melakukan tugas CPU-intensif (seperti memproses berkas gambar besar, mengompresi data, atau mendekode dokumen). Kita memindahkan kalkulasi tersebut ke Isolate anak agar thread rendering utama (Main Isolate) tetap lancar di 60fps/120fps.

Berikut adalah implementasi komputasi asinkron menggunakan Isolate.spawn() lengkap dengan pengiriman progres berkala dan penanganan kesalahan:

// lib/core/background/file_processor_isolate.dart
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:flutter/foundation.dart';

class FileProcessorIsolate {
  /// Memproses baris teks dalam file besar secara asinkron di Isolate terpisah.
  /// Memancarkan nilai progres (0.0 hingga 1.0) melalui Stream.
  static Stream<double> prosesFileLatarBelakang({
    required String pathFileSumber,
    required String pathFileTujuan,
  }) {
    final StreamController<double> controllerProgres = StreamController<double>();
    final ReceivePort portPenerimaDart = ReceivePort();

    // Jalankan isolate anak
    Isolate.spawn<InitParams>(
      _entriPointIsolate,
      InitParams(
        sendPort: portPenerimaDart.sendPort,
        pathFileSumber: pathFileSumber,
        pathFileTujuan: pathFileTujuan,
      ),
    ).then((Isolate isolateAnak) {
      // Dengarkan pesan yang dikirim dari isolate anak
      portPenerimaDart.listen((dynamic pesan) {
        if (pesan is double) {
          // Update progres
          controllerProgres.add(pesan);
        } else if (pesan is String && pesan == 'SELESAI') {
          // Tutup komunikasi jika selesai
          portPenerimaDart.close();
          isolateAnak.kill(priority: Isolate.immediate);
          controllerProgres.close();
        } else if (pesan is String && pesan.startsWith('ERROR:')) {
          // Kirim error ke stream listener
          portPenerimaDart.close();
          isolateAnak.kill(priority: Isolate.immediate);
          controllerProgres.addError(Exception(pesan.replaceFirst('ERROR:', '')));
          controllerProgres.close();
        }
      });
    }).catchError((dynamic err) {
      controllerProgres.addError(err);
      controllerProgres.close();
    });

    return controllerProgres.stream;
  }
}

// Model parameter inisialisasi Isolate
class InitParams {
  final SendPort sendPort;
  final String pathFileSumber;
  final String pathFileTujuan;

  InitParams({
    required this.sendPort,
    required this.pathFileSumber,
    required this.pathFileTujuan,
  });
}

// Titik masuk Isolate anak. Harus berupa fungsi top-level atau static.
void _entriPointIsolate(InitParams params) async {
  try {
    final fileSumber = File(params.pathFileSumber);
    if (!await fileSumber.exists()) {
      params.sendPort.send('ERROR:File sumber tidak ditemukan.');
      return;
    }

    final barisTeks = await fileSumber.readAsLines();
    final totalBaris = barisTeks.length;
    final fileTujuan = File(params.pathFileTujuan);
    
    // Buka sink penulisan file
    final iosink = fileTujuan.openWrite();
    
    for (var i = 0; i < totalBaris; i++) {
      // Lakukan pemrosesan teks berat (misal: normalisasi dan enkripsi tiruan)
      final stringHasil = _prosesTextBerat(barisTeks[i]);
      iosink.write('$stringHasil\n');

      // Kirim pembaruan progres setiap 2% pemrosesan untuk mengurangi overhead komunikasi
      if (i % (totalBaris ~/ 50 + 1) == 0) {
        final double progres = (i + 1) / totalBaris;
        params.sendPort.send(progres);
      }
    }

    await iosink.flush();
    await iosink.close();

    // Kirim sinyal selesai
    params.sendPort.send('SELESAI');
  } catch (e) {
    params.sendPort.send('ERROR:${e.toString()}');
  }
}

String _prosesTextBerat(String input) {
  // Simulasi enkripsi teks menggunakan manipulasi string kompleks
  return input.trim().split('').reversed.join('|').toUpperCase();
}

Pola 2: Workmanager (Pekerjaan Terjadwal & Headless) #

Pola kedua ini memanfaatkan penjadwal native sistem operasi—JobScheduler/WorkManager di Android dan BGTaskScheduler di iOS. Pola ini sangat ideal untuk tugas-tugas berkala (periodic tasks) seperti menyinkronkan data database lokal ke server cloud setiap beberapa jam sekali, atau mengirim log analitik yang tertunda.

1. Persiapan Konfigurasi Platform Native #

A. Konfigurasi Android #

Untuk kebutuhan dasar workmanager, kita tidak membutuhkan modifikasi manifes khusus. Namun, jika tugas kita memerlukan koneksi internet, pastikan izin internet sudah terpasang di android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />

B. Konfigurasi iOS #

Buka folder proyek iOS di Xcode:

  1. Pilih target Runner $\rightarrow$ tab Signing & Capabilities.
  2. Klik + Capability $\rightarrow$ tambahkan Background Modes.
  3. Centang opsi Background fetch dan Background processing.
  4. Buka berkas ios/Runner/Info.plist, tambahkan baris berikut di dalam tag <dict> untuk meregistrasikan ID unik untuk tugas background kita:
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.unisbadri.flutter.syncTask</string>
</array>

2. Implementasi Kode di Flutter #

Penting untuk menandai fungsi dispatcher dengan anotasi @pragma('vm:entry-point'). Ini memberi tahu kompilator Dart AOT agar tidak menghapus fungsi ini selama proses optimalisasi minifikasi kode (tree shaking), karena fungsi ini akan dipanggil secara dinamis langsung dari kode native Android/iOS saat aplikasi ditutup.

// lib/core/background/workmanager_service.dart
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:workmanager/workmanager.dart';

// ID unik tugas untuk referensi sistem
const String namaTugasSync = "com.unisbadri.flutter.syncTask";

/// Callback Dispatcher yang dieksekusi oleh OS saat trigger background aktif.
/// Harus diletakkan sebagai fungsi top-level di luar kelas.
@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask((String namaTugas, Map<String, dynamic>? inputData) async {
    // PENTING: Di dalam scope ini, kita berjalan di Headless Isolate terpisah.
    // Tidak ada widget tree, tidak ada active provider UI.
    
    switch (namaTugas) {
      case namaTugasSync:
        return await _eksekusiTugasSinkronisasi(inputData);
      default:
        return false;
    }
  });
}

Future<bool> _eksekusiTugasSinkronisasi(Map<String, dynamic>? inputData) async {
  try {
    debugPrint('[Workmanager] Mulai sinkronisasi data di latar belakang...');
    
    // Inisialisasi ulang penyimpanan lokal (SharedPreferences bersifat thread-safe)
    final prefs = await SharedPreferences.getInstance();
    
    // Mengambil timestamp sinkronisasi terakhir
    final lastSync = prefs.getInt('last_sync_timestamp') ?? 0;
    debugPrint('[Workmanager] Terakhir sinkronisasi: ${DateTime.fromMillisecondsSinceEpoch(lastSync)}');

    // Jalankan request HTTP (simulasi)
    final statusBerhasil = await _kirimDataKeServer(inputData);
    
    if (statusBerhasil) {
      await prefs.setInt('last_sync_timestamp', DateTime.now().millisecondsSinceEpoch);
      debugPrint('[Workmanager] Sinkronisasi selesai dengan sukses.');
      return true; // Mengembalikan true agar OS tahu task berhasil
    }
    return false; // Mengembalikan false agar OS melakukan penjadwalan ulang (retry)
  } catch (e) {
    debugPrint('[Workmanager] Gagal menjalankan sinkronisasi: $e');
    return false;
  }
}

Future<bool> _kirimDataKeServer(Map<String, dynamic>? inputData) async {
  // Simulasi pengiriman data jaringan
  await Future.delayed(const Duration(seconds: 4));
  return true;
}

class WorkmanagerService {
  /// Inisialisasi awal Workmanager
  static Future<void> inisialisasi() async {
    await Workmanager().initialize(
      callbackDispatcher,
      isInDebugMode: kDebugMode, // Memunculkan notifikasi debug di Android
    );
  }

  /// Mendaftarkan tugas berkala untuk menyinkronkan data setiap 15 menit
  static Future<void> daftarkanTugasSyncBerkala() async {
    await Workmanager().registerPeriodicTask(
      "sync-data-id-unik", // ID unik internal Workmanager
      namaTugasSync,
      frequency: const Duration(minutes: 15), // Batas minimum frekuensi Android/iOS adalah 15 menit
      constraints: Constraints(
        networkType: NetworkType.connected, // Hanya jalan jika perangkat terkoneksi internet
        requiresBatteryNotLow: true,        // Jangan jalankan jika baterai lemah
        requiresCharging: false,
      ),
      existingWorkPolicy: ExistingWorkPolicy.keep, // Jaga tugas yang sudah terdaftar
    );
  }

  /// Membatalkan seluruh tugas latar belakang terdaftar
  static Future<void> matikanSemuaTugas() async {
    await Workmanager().cancelAll();
    debugPrint('[Workmanager] Semua tugas latar belakang telah dibatalkan.');
  }
}

Pola 3: Background Service (Tugas Jangka Panjang Terus-Menerus) #

Ketika aplikasi kita membutuhkan pemrosesan latar belakang yang berjalan terus-menerus tanpa jeda—seperti melacak rute lari pengguna melalui koordinat GPS secara real-time atau memutar playlist audio—penjadwalan berkala seperti Workmanager tidak lagi memadai. Kita memerlukan Background Service.

Pada platform Android, tugas berdurasi panjang wajib berjalan sebagai Foreground Service yang menampilkan notifikasi persisten yang tidak dapat dihapus oleh pengguna. Notifikasi ini berfungsi memberi tahu pengguna bahwa aplikasi kita sedang mengonsumsi daya baterai aktif di latar belakang. Pada platform iOS, tugas semacam ini sangat dibatasi dan hanya diizinkan untuk kategori tertentu (seperti navigasi GPS, pemutaran audio, VoIP, dan sinkronisasi Bluetooth eksternal).

Kita akan mengimplementasikanBackground Service menggunakan package flutter_background_service.

1. Konfigurasi Native Android #

Tambahkan izin layanan latar belakang di android/app/src/main/AndroidManifest.xml:

<!-- Izin untuk menjalankan foreground service -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<application ...>
    <!-- Daftarkan service background -->
    <service
        android:name="id.flutter.flutter_background_service.BackgroundService"
        android:foregroundServiceType="location"
        android:exported="false" />
</application>

2. Implementasi di Flutter #

Berikut adalah implementasi lengkap untuk Background Service yang mengirimkan pembaruan koordinat tiruan dari latar belakang ke halaman UI secara real-time:

// lib/core/background/tracking_background_service.dart
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_background_service_android/flutter_background_service_android.dart';

class TrackingBackgroundService {
  static const String channelIdNotifikasi = 'my_tracking_channel';
  static const int idNotifikasi = 999;

  /// Inisialisasi awal konfigurasi service
  static Future<void> inisialisasi() async {
    final service = FlutterBackgroundService();

    await service.configure(
      androidConfiguration: AndroidConfiguration(
        onStart: _entriPointService,
        autoStart: false, // Jangan jalankan otomatis, tunggu pemicuan UI
        isForegroundMode: true,
        notificationChannelId: channelIdNotifikasi,
        initialNotificationTitle: 'Pelacakan Rute Aktif',
        initialNotificationContent: 'Menunggu sinyal GPS...',
        foregroundServiceNotificationId: idNotifikasi,
      ),
      iosConfiguration: IosConfiguration(
        autoStart: false,
        onForeground: _entriPointService,
        onBackground: _onIosBackgroundFallback,
      ),
    );
  }

  /// Memulai layanan latar belakang
  static Future<void> mulaiLayanan() async {
    final service = FlutterBackgroundService();
    final statusBerjalan = await service.isRunning();
    if (!statusBerjalan) {
      await service.startService();
    }
  }

  /// Menghentikan layanan latar belakang
  static void hentikanLayanan() {
    final service = FlutterBackgroundService();
    service.invoke('stopService');
  }
}

// Handler utama saat service background Android/iOS aktif
@pragma('vm:entry-point')
void _entriPointService(ServiceInstance service) async {
  // Pastikan plugin engine terhubung dengan baik ke platform host
  DartPluginRegistrant.ensureInitialized();

  if (service is AndroidServiceInstance) {
    service.on('setAsForeground').listen((event) {
      service.setAsForegroundService();
    });

    service.on('setAsBackground').listen((event) {
      service.setAsBackgroundService();
    });
  }

  // Dengarkan pemicu penghentian dari UI
  service.on('stopService').listen((event) {
    service.stopSelf();
  });

  // Contoh pelacakan berkala (Simulasi emulasi GPS koordinat)
  double latitude = -6.2088;
  double longitude = 106.8456;

  Timer.periodic(const Duration(seconds: 3), (timer) async {
    final statusAktif = await service.isRunning();
    if (!statusAktif) {
      timer.cancel();
      return;
    }

    // Ubah titik koordinat secara acak seolah pengguna sedang bergerak
    latitude += 0.0001;
    longitude += 0.0001;

    // 1. Update info pada notifikasi sistem Android
    if (service is AndroidServiceInstance) {
      if (await service.isForegroundService()) {
        service.setForegroundNotificationInfo(
          title: 'Pelacakan Rute Berjalan',
          content: 'Koordinat: ${latitude.toStringAsFixed(5)}, ${longitude.toStringAsFixed(5)}',
        );
      }
    }

    // 2. Kirim pesan event ke UI thread utama (jika aplikasi sedang terbuka)
    service.invoke(
      'updateKoordinat',
      {
        'latitude': latitude,
        'longitude': longitude,
        'timestamp': DateTime.now().toIso8601String(),
      },
    );
  });
}

@pragma('vm:entry-point')
Future<bool> _onIosBackgroundFallback(ServiceInstance service) async {
  return true;
}

Berikut adalah contoh bagaimana kita memantau dan mengontrol Background Service ini dari halaman widget UI kita:

// lib/presentation/pages/tracking_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import '../../core/background/tracking_background_service.dart';

class TrackingPage extends StatefulWidget {
  const TrackingPage({super.key});

  @override
  State<TrackingPage> createState() => _TrackingPageState();
}

class _TrackingPageState extends State<TrackingPage> {
  String _statusStatus = 'Layanan Mati';
  String _posisiTerakhir = 'Belum ada data';
  StreamSubscription? _sub;

  @override
  void initState() {
    super.initState();
    _inisialisasiListener();
  }

  void _inisialisasiListener() {
    // Hubungkan listener ke channel event 'updateKoordinat' yang dipancarkan service
    _sub = FlutterBackgroundService().on('updateKoordinat').listen((Map<String, dynamic>? event) {
      if (event != null && mounted) {
        final double lat = event['latitude'] as double;
        final double lng = event['longitude'] as double;
        setState(() {
          _statusStatus = 'Layanan Berjalan';
          _posisiTerakhir = 'Lat: ${lat.toStringAsFixed(6)}, Lng: ${lng.toStringAsFixed(6)}';
        });
      }
    });
  }

  @override
  void dispose() {
    _sub?.cancel(); // Pastikan subscription dibersihkan
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Pelacakan GPS Latar Belakang')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Status Layanan: $_statusStatus', style: const TextStyle(fontSize: 18)),
            const SizedBox(height: 8),
            Text('Posisi Saat Ini:\n$_posisiTerakhir', textAlign: TextAlign.center),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: () async {
                await TrackingBackgroundService.mulaiLayanan();
                setState(() => _statusStatus = 'Memulai Layanan...');
              },
              child: const Text('Mulai Pelacakan'),
            ),
            const SizedBox(height: 12),
            ElevatedButton(
              onPressed: () {
                TrackingBackgroundService.hentikanLayanan();
                setState(() {
                  _statusStatus = 'Layanan Mati';
                  _posisiTerakhir = 'Dihentikan';
                });
              },
              style: ElevatedButton.styleFrom(backgroundColor: Colors.red.shade100),
              child: const Text('Hentikan Pelacakan', style: TextStyle(color: Colors.red)),
            ),
          ],
        ),
      ),
    );
  }
}

Batasan Ketat Sistem Operasi (Android & iOS) #

Sistem operasi mobile menerapkan kebijakan alokasi daya yang dinamis untuk memastikan efisiensi baterai. Sebagai pengembang aplikasi, kita wajib mematuhi aturan-aturan batasan berikut guna mencegah tugas latar belakang kita dihentikan paksa:

1. Kebijakan Batasan pada Android #

  • Doze Mode: Diperkenalkan sejak Android 6.0, fitur ini membatasi aktivitas jaringan dan akses CPU aplikasi secara total jika perangkat dibiarkan diam tidak bergerak dalam waktu lama. Semua eksekusi Workmanager akan ditangguhkan hingga perangkat terbangun atau masuk ke dalam jendela pemeliharaan (maintenance window). Kita dapat mensimulasikan mode ini saat pengujian menggunakan perintah terminal:
    # Memaksa Android masuk ke status idle Doze Mode
    adb shell dumpsys deviceidle force-idle
    
  • App Standby Buckets: Sistem operasi Android mengategorikan aplikasi kita ke dalam beberapa kelompok prioritas penggunaan berdasarkan seberapa sering pengguna membuka aplikasi tersebut. Aplikasi yang jarang dibuka akan dimasukkan ke dalam bucket Rare yang memiliki batas kuota eksekusi latar belakang terkecil.
  • Pembatalan Optimasi Baterai: Untuk tugas esensial yang tidak boleh terputus, kita dapat mengarahkan pengguna untuk mengecualikan aplikasi kita dari fitur optimasi baterai internal Android.

2. Kebijakan Batasan pada iOS #

  • BGTaskScheduler Resource Limits: Penjadwal latar belakang iOS (BGTaskScheduler) hanya memberikan alokasi waktu eksekusi yang sangat singkat, yaitu sekitar 30 detik untuk setiap pemanggilan tugas. Jika kode kita tidak selesai dieksekusi dan tidak memanggil penyelesaian tugas (task completion completion handler) dalam batas waktu tersebut, iOS akan menghentikan paksa proses aplikasi kita.
  • Dynamic OS Execution Decider: Berbeda dengan Android yang memiliki interval waktu eksekusi terjadwal yang relatif konsisten, iOS mendeteksi pola kebiasaan pengguna. Jika pengguna biasanya membuka aplikasi kita pada jam 8 pagi, iOS akan menjadwalkan eksekusi tugas background fetch kita beberapa menit sebelum jam 8 pagi. OS sepenuhnya memegang kendali kapan tugas akan dijalankan; aplikasi tidak memiliki wewenang untuk meminta eksekusi tepat waktu (exact timing).

Pola Desain Terbaik (Best Practices) untuk Background Tasks #

Untuk merancang sistem eksekusi latar belakang yang tangguh di lingkungan Flutter, terapkan prinsip-prinsip desain berikut:

1. Terapkan Sifat Idempotensi pada Setiap Tugas #

Tugas latar belakang kita harus bersifat idempotent, artinya jika tugas tersebut dijalankan berulang kali dengan input yang sama, hasil akhirnya akan tetap konsisten tanpa menduplikasi data. Batasan jaringan seluler yang tidak stabil sering kali membuat koneksi terputus di tengah jalan. Jika tugas pengiriman transaksi kita terputus dan OS mencoba menjalankannya ulang pada kesempatan berikutnya, pastikan database server kita tidak mencatat transaksi ganda.

2. Gunakan Strategi Checkpointing (Penyimpanan Status Parsial) #

Jangan menunggu seluruh tugas selesai baru menyimpan status akhir ke penyimpanan lokal. Jika kita memiliki tugas sinkronisasi gambar sebanyak 50 file, simpan status keberhasilan setiap kali satu file berhasil diunggah ke database SQLite atau SharedPreferences. Jika sistem operasi tiba-tiba mematikan aplikasi kita pada file ke-30 karena keterbatasan memori, kita dapat melanjutkan proses sinkronisasi dari file ke-31 saat tugas dipicu kembali oleh OS, bukan memulainya ulang dari file pertama.

3. Jaga Memory Footprint Minimal (Lightweight Background Isolate) #

Headless Isolate yang berjalan di latar belakang tidak memerlukan grafik, asset dekoratif, atau widget visual. Hindari melakukan inisialisasi modul pihak ketiga yang tidak relevan dengan tugas latar belakang tersebut. Semakin kecil konsumsi RAM yang digunakan oleh isolate latar belakang kita, semakin kecil kemungkinan sistem operasi menghentikan paksa proses aplikasi kita saat RAM perangkat menipis.


Ringkasan #

  • Arsitektur Isolate Latar Belakang: Kode latar belakang berjalan di dalam headless isolate terpisah yang tidak memiliki akses ke state UI, singletons di Main Isolate, atau BuildContext. Dependensi harus diinisialisasi ulang dari awal.
  • Pola Isolate: Cocok untuk memindahkan beban kerja komputasi CPU berat (parsing data, enkripsi citra) saat aplikasi aktif di foreground agar UI tetap responsif.
  • Pola Workmanager: Solusi terbaik untuk sinkronisasi data terjadwal atau satu kali eksekusi yang dijamin berjalan oleh OS meskipun aplikasi ditutup. Batasan frekuensi minimum adalah 15 menit.
  • Pola Background Service: Digunakan untuk tugas terus-menerus tanpa jeda (seperti navigasi GPS atau pemutaran musik). Di Android wajib berjalan sebagai Foreground Service dengan notifikasi persisten.
  • Prinsip Desain Tangguh: Pastikan setiap tugas latar belakang bersifat idempotent, hemat penggunaan memori, dan selalu lakukan penyimpanan status parsial (checkpointing) untuk mengatasi penghentian paksa oleh OS.

← Sebelumnya: Platform Channels   Berikutnya: Push Notification & Deep Link →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact