Background Tasks #
Menjalankan kode saat app tidak di foreground adalah salah satu area paling rumit di mobile development. Android dan iOS memiliki mekanisme yang berbeda dan batasan yang terus diperketat di setiap versi OS untuk menghemat baterai. Memahami kapan menggunakan tool yang tepat — workmanager, isolate, atau background service — menentukan apakah background task kamu benar-benar berjalan atau diam-diam dibatalkan oleh sistem.
Tiga Pola Background Task #
ISOLATE (compute/Isolate.spawn):
✓ Komputasi CPU-intensif (parsing, enkripsi, ML inference)
✓ Berjalan di thread terpisah, MASIH dalam proses app (foreground)
✗ Tidak berjalan saat app ditutup pengguna
Contoh: resize gambar, parse JSON besar, generate PDF
WORKMANAGER:
✓ Periodic task (sync data setiap X menit/jam)
✓ One-off task yang dijadwalkan (kirim analytics)
✓ Berjalan meski app ditutup (dijamin oleh OS)
✗ Tidak bisa dijalankan pada waktu yang tepat (OS memilih kapan)
✗ Tidak bisa lama-lama (iOS: ~30 detik, Android: ~10 menit)
Contoh: sync harian, upload pending di background
BACKGROUND SERVICE (flutter_background_service):
✓ Long-running task yang harus terus berjalan
✓ Berjalan meski app diminimize
✗ Android: butuh foreground notification yang terlihat pengguna
✗ iOS: sangat terbatas (audio, location, atau VOIP saja)
Contoh: tracking GPS terus-menerus, streaming audio, pedometer
Workmanager — Background Task Terjadwal #
# pubspec.yaml
dependencies:
workmanager: ^0.5.2
Setup #
// android/app/src/main/AndroidManifest.xml
// Tidak perlu permission tambahan untuk workmanager basic
// iOS -- aktifkan Background Modes di Xcode:
// Runner → Signing & Capabilities → + Capability → Background Modes
// Centang: Background fetch, Background processing
// main.dart
import 'package:workmanager/workmanager.dart';
// Callback yang berjalan di background -- harus top-level function
@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((taskName, inputData) async {
// Di sini app berjalan di isolate terpisah
// TIDAK ada BuildContext, TIDAK ada Provider
switch (taskName) {
case 'syncData':
await _syncDataTask(inputData);
return true; // true = berhasil, false = gagal (akan dicoba ulang)
case 'uploadPending':
await _uploadPendingTask(inputData);
return true;
default:
return false;
}
});
}
Future<void> _syncDataTask(Map<String, dynamic>? inputData) async {
// Buat instance storage/service secara manual (tidak ada DI)
final prefs = await SharedPreferences.getInstance();
final apiClient = ApiClient(baseUrl: 'https://api.contoh.com');
try {
final data = await apiClient.fetchData();
await prefs.setString('cached_data', jsonEncode(data));
debugPrint('[WorkManager] Sync berhasil: ${data.length} item');
} catch (e) {
debugPrint('[WorkManager] Sync gagal: $e');
rethrow; // lempar ulang agar workmanager tahu task gagal
}
}
// Di main()
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Workmanager().initialize(
callbackDispatcher,
isInDebugMode: kDebugMode, // log saat debug
);
runApp(const MyApp());
}
Mendaftarkan Task #
class BackgroundTaskService {
// Periodic task -- jalan setiap 15 menit (minimum iOS: 15 menit)
static Future<void> registerSyncTask() async {
await Workmanager().registerPeriodicTask(
'syncData-unique-id', // ID unik untuk task ini
'syncData', // taskName yang diterima callbackDispatcher
frequency: const Duration(minutes: 15),
constraints: Constraints(
networkType: NetworkType.connected, // hanya jalan saat ada internet
requiresBatteryNotLow: true, // jangan jalan saat baterai kritis
requiresCharging: false,
),
existingWorkPolicy: ExistingWorkPolicy.replace, // ganti jika sudah ada
backoffPolicy: BackoffPolicy.exponential,
backoffPolicyDelay: const Duration(minutes: 1),
);
}
// One-off task -- jalan sekali, dijadwalkan untuk nanti
static Future<void> scheduleUpload(Map<String, dynamic> pendingData) async {
await Workmanager().registerOneOffTask(
'uploadPending-${DateTime.now().millisecondsSinceEpoch}',
'uploadPending',
inputData: pendingData,
initialDelay: const Duration(minutes: 5), // tunggu 5 menit
constraints: Constraints(networkType: NetworkType.connected),
);
}
// Batalkan task
static Future<void> cancelSync() async {
await Workmanager().cancelByUniqueName('syncData-unique-id');
}
static Future<void> cancelAll() async {
await Workmanager().cancelAll();
}
}
Isolate — Komputasi di Thread Terpisah #
Untuk operasi berat yang dilakukan saat app masih di foreground, gunakan Isolate agar tidak memblokir UI:
// CARA 1: compute() -- untuk operasi sekali dengan hasil
// (sudah dibahas di artikel Memory & Size)
final hasil = await compute(parseProdukBesar, jsonString);
// CARA 2: Isolate dengan komunikasi dua arah
// Untuk operasi yang mengirim progress atau banyak hasil
Future<void> prosesFileBesar(String filePath) async {
final receivePort = ReceivePort();
final isolate = await Isolate.spawn(
_prosesFileIsolate,
_ProsesParams(
sendPort: receivePort.sendPort,
filePath: filePath,
),
);
// Terima pesan dari isolate
await for (final message in receivePort) {
if (message is double) {
// Progress 0.0 - 1.0
print('Progress: ${(message * 100).toStringAsFixed(0)}%');
} else if (message is List<String>) {
// Hasil akhir
print('Selesai: ${message.length} baris');
receivePort.close();
isolate.kill();
break;
} else if (message is String && message == 'error') {
receivePort.close();
isolate.kill();
throw Exception('Isolate error');
}
}
}
class _ProsesParams {
final SendPort sendPort;
final String filePath;
_ProsesParams({required this.sendPort, required this.filePath});
}
void _prosesFileIsolate(_ProsesParams params) async {
try {
final file = File(params.filePath);
final lines = await file.readAsLines();
final hasil = <String>[];
for (var i = 0; i < lines.length; i++) {
// Proses setiap baris
hasil.add(lines[i].trim().toUpperCase());
// Kirim progress setiap 100 baris
if (i % 100 == 0) {
params.sendPort.send(i / lines.length);
}
}
params.sendPort.send(hasil); // kirim hasil akhir
} catch (e) {
params.sendPort.send('error');
}
}
Background Service — Long-Running Task #
Untuk task yang harus terus berjalan lama (GPS tracking, streaming audio):
# pubspec.yaml
dependencies:
flutter_background_service: ^5.0.5
flutter_local_notifications: ^17.2.4 # untuk foreground notification di Android
// lib/core/background/background_service.dart
import 'package:flutter_background_service/flutter_background_service.dart';
Future<void> initializeBackgroundService() async {
final service = FlutterBackgroundService();
await service.configure(
androidConfiguration: AndroidConfiguration(
onStart: onServiceStart,
autoStart: false,
isForegroundMode: true, // wajib Android 8+
notificationChannelId: 'app_service',
initialNotificationTitle: 'App Berjalan',
initialNotificationContent: 'Memantau lokasi...',
foregroundServiceNotificationId: 888,
),
iosConfiguration: IosConfiguration(
autoStart: false,
onForeground: onServiceStart,
onBackground: onIosBackground, // iOS background handler
),
);
}
// Handler yang berjalan di background -- top-level function
@pragma('vm:entry-point')
void onServiceStart(ServiceInstance service) async {
DartPluginRegistrant.ensureInitialized();
// Kirim update ke UI setiap 5 detik
Timer.periodic(const Duration(seconds: 5), (timer) async {
if (service is AndroidServiceInstance) {
if (await service.isForegroundService()) {
// Update notifikasi foreground
service.setForegroundNotificationInfo(
title: 'Tracking Aktif',
content: 'Posisi: ${DateTime.now()}',
);
}
}
// Kirim data ke UI (jika app terbuka)
service.invoke('update', {
'timestamp': DateTime.now().toIso8601String(),
'latitude': -6.2088,
'longitude': 106.8456,
});
});
// Stop service dari UI
service.on('stop').listen((_) {
service.stopSelf();
timer.cancel();
});
}
@pragma('vm:entry-point')
Future<bool> onIosBackground(ServiceInstance service) async {
WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized();
return true;
}
// Kontrol service dari UI
class LocationTrackingWidget extends ConsumerWidget {
void _startTracking() async {
final service = FlutterBackgroundService();
await service.startService();
// Dengarkan update dari service
service.on('update').listen((data) {
// update UI dengan data terbaru
});
}
void _stopTracking() {
FlutterBackgroundService().invoke('stop');
}
}
Batasan OS yang Harus Diketahui #
ANDROID:
- Android 8+: Background task biasa dibatasi (Doze mode)
- Android 12+: Exact alarms membutuhkan permission khusus
- WorkManager: dijamin jalan, tapi OS memilih kapan (bisa delay)
- Foreground service: wajib tampilkan notifikasi yang terlihat
- Background process limit: ~6 cached processes, sisanya di-kill
iOS:
- Background fetch: OS panggil app sesekali (tidak bisa ditentukan kapan)
- BGTaskScheduler: iOS 13+ untuk background processing
- Long-running: hanya untuk audio, VOIP, location, accessories, Bluetooth
- Tidak ada WorkManager-equivalent -- iOS jauh lebih ketat
- Background task di-kill jika terlalu lama (~30 detik untuk fetch)
BEST PRACTICES:
✓ Buat task sesingkat mungkin
✓ Simpan checkpoint progress sehingga bisa dilanjutkan jika di-kill
✓ Jangan asumsikan task pasti jalan pada waktu yang tepat
✓ Selalu test di device fisik -- emulator tidak merepresentasikan batasan nyata
✓ Test dengan Doze mode aktif (Android): adb shell dumpsys deviceidle force-idle
Ringkasan #
- Tiga pola:
Isolate/computeuntuk komputasi berat di foreground,workmanageruntuk task terjadwal yang berjalan meski app ditutup,flutter_background_serviceuntuk long-running task yang terus berjalan.compute()adalah cara termudah untuk pindahkan logika berat ke isolate — cukup satu function call, hasilnya dikembalikan via Future.- WorkManager menjamin task berjalan tapi tidak menjamin kapan — OS memilih waktu yang efisien untuk baterai. Jangan gunakan untuk task yang harus tepat waktu.
- Foreground service di Android wajib menampilkan notifikasi yang terlihat pengguna — iOS memiliki batasan yang jauh lebih ketat dan hanya mendukung kategori tertentu.
callbackDispatcherdan semua top-level function yang dijalankan di background harus di-annotate dengan@pragma('vm:entry-point')agar tidak ter-tree-shake saat build release.- Selalu test di device fisik dengan kondisi nyata (baterai rendah, Doze mode aktif) — emulator tidak mensimulasikan batasan OS yang sebenarnya.
- Simpan checkpoint progress setiap beberapa langkah sehingga task bisa dilanjutkan jika di-kill oleh OS sebelum selesai.
← Sebelumnya: Platform Channels Berikutnya: Push Notification & Deep Link →