Tugas latar belakang yang harus dijamin selesai (mis. upload foto, sinkronisasi data) tidak boleh memakai coroutine biasa — Activity dapat di-destroy dan process dapat dimatikan oleh sistem. WorkManager adalah library Jetpack yang menjadwalkan tugas deferrable dengan jaminan: tugas akan dijalankan walau aplikasi keluar, walau peranti restart, dan sesuai constraints yang ditetapkan. Bab ini menjelaskan OneTimeWorkRequest, PeriodicWorkRequest, constraints, dan backoff policy.

OneTimeWorkRequest vs PeriodicWorkRequest #

OneTimeWorkRequest menjalankan tugas satu kali. PeriodicWorkRequest menjalankan tugas berulang dengan interval minimum 15 menit (batas sistem). Keduanya mendukung constraints, tag, dan backoff.

kotlin Definisi Worker
class UploadWorker(appContext: Context, params: WorkerParameters) : CoroutineWorker(appContext, params) {
    override suspend fun doWork(): Result {
        return try {
            uploadFoto(inputData.getInt("FOTO_ID", -1))
            Result.success(workDataOf("URL" to hasilUrl))
        } catch (e: Exception) {
            Result.retry()
        }
    }
}

val request = OneTimeWorkRequestBuilder<UploadWorker>()
    .setInputData(workDataOf("FOTO_ID" to 42))
    .setConstraints(Constraints.Builder().setRequiredNetworkType(UNMETERED).build())
    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
    .build()

WorkManager.getInstance(context).enqueue(request)

Tipe Result menentukan tindakan WorkManager: success menyelesaikan tugas, failure menghentikan tanpa retry, retry menjadwalkan ulang dengan backoff.

Constraints #

Constraints membatasi kapan tugas boleh berjalan. Tanpa constraints, WorkManager menjalankan tugas sesegera mungkin. Dengan constraints, tugas menunggu sampai semua kondisi terpenuhi.

ConstraintKapan dipakai
setRequiredNetworkType(UNMETERED)Backup, sync besar (jangan di data seluler)
setRequiresCharging(true)Tugas berat (encode video, training model)
setRequiresIdle(true)Maintenance database, vacuum
setRequiresBatteryNotLow(true)Hindari tugas yang boros baterai

Backoff Policy #

Saat worker mengembalikan Result.retry(), WorkManager menjadwalkan ulang dengan backoff. Dua pilihan: LINEAR (interval tetap) dan EXPONENTIAL (interval berlipat ganda). Backoff default adalah 30 detik dengan exponential.

kotlin Backoff exponential
val work = OneTimeWorkRequestBuilder<SyncWorker>()
    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
    .build()

Urutan retry dengan exponential: 30s, 60s, 120s, 240s, dst. Backoff mencegah server terbebani oleh worker yang selalu gagal dan segera mencoba ulang.

Simulasi WorkManager Queue #

Cuplikan berikut memodelkan WorkManager sebagai queue yang memeriksa constraints sebelum menjalankan worker. Worker yang gagal constraint di-skip dan di-enqueue ulang:

kotlin
// background_workmanager.kt
// Mensimulasikan WorkManager: OneTimeWorkRequest, PeriodicWorkRequest,
// Constraints (network, charging), dan backoff policy. Tidak memerlukan
// Android SDK — hanya memetakan kontrak WorkManager sebagai queue.

data class WorkRequest(
    val id: String,
    val type: String,  // "OneTime" atau "Periodic"
    val constraints: WorkConstraints,
    val backoffMs: Long = 1000L,
    val maxRetries: Int = 3
)

data class WorkConstraints(
    val requiresNetwork: Boolean = false,
    val requiresCharging: Boolean = false,
    val requiresIdle: Boolean = false
)

data class WorkResult(val id: String, val success: Boolean, val attempts: Int)

class WorkManager(
    private val systemState: () -> WorkConstraints = { WorkConstraints(requiresNetwork = true, requiresCharging = false, requiresIdle = false) }
) {
    private val queue = ArrayDeque<WorkRequest>()
    private val results = mutableListOf<WorkResult>()
    private val workerStatus = mutableMapOf<String, Int>()  // id -> attempt count

    fun enqueue(request: WorkRequest) {
        queue.addLast(request)
        println("  [Enqueue] ${request.id} (${request.type}) constraints=${request.constraints}")
    }

    fun runNext(): WorkResult? {
        val req = queue.removeFirstOrNull() ?: return null
        val attempts = (workerStatus[req.id] ?: 0) + 1
        workerStatus[req.id] = attempts

        // Cek constraints
        val sys = systemState()
        val constraintsMet = (!req.constraints.requiresNetwork || sys.requiresNetwork) &&
                              (!req.constraints.requiresCharging || sys.requiresCharging) &&
                              (!req.constraints.requiresIdle || sys.requiresIdle)

        if (!constraintsMet) {
            println("  [Skip] ${req.id} constraints belum terpenuhi, retry")
            if (attempts < req.maxRetries) {
                queue.addLast(req)  // re-enqueue
            }
            return null
        }

        // Eksekusi worker (selalu sukses untuk demo)
        val result = WorkResult(req.id, success = true, attempts = attempts)
        results.add(result)
        println("  [Run] ${req.id} success=true attempts=$attempts")
        return result
    }

    fun dumpResults(): List<WorkResult> = results.toList()
}

fun main() {
    val wm = WorkManager()

    println("=== Enqueue 3 work requests ===")
    wm.enqueue(WorkRequest("upload-foto-1", "OneTime", WorkConstraints(requiresNetwork = true)))
    wm.enqueue(WorkRequest("sync-tugas", "Periodic", WorkConstraints(requiresNetwork = true, requiresCharging = true)))
    wm.enqueue(WorkRequest("compress-cache", "OneTime", WorkConstraints()))

    println("\n=== Run semua (state sistem: network=true, charging=false) ===")
    repeat(5) { wm.runNext() }

    println("\n=== Hasil ===")
    wm.dumpResults().forEach { println("  $it") }

    println("\n=== State sistem berubah: charging=true ===")
    val wm2 = WorkManager { WorkConstraints(requiresNetwork = true, requiresCharging = true, requiresIdle = false) }
    wm2.enqueue(WorkRequest("sync-tugas", "Periodic", WorkConstraints(requiresNetwork = true, requiresCharging = true)))
    wm2.runNext()
    wm2.dumpResults().forEach { println("  $it") }
}

Output yang Dihasilkan:

text
=== Enqueue 3 work requests ===
  [Enqueue] upload-foto-1 (OneTime) constraints=WorkConstraints(requiresNetwork=true, requiresCharging=false, requiresIdle=false)
  [Enqueue] sync-tugas (Periodic) constraints=WorkConstraints(requiresNetwork=true, requiresCharging=true, requiresIdle=false)
  [Enqueue] compress-cache (OneTime) constraints=WorkConstraints(requiresNetwork=false, requiresCharging=false, requiresIdle=false)

=== Run semua (state sistem: network=true, charging=false) ===
  [Run] upload-foto-1 success=true attempts=1
  [Skip] sync-tugas constraints belum terpenuhi, retry
  [Run] compress-cache success=true attempts=1
  [Skip] sync-tugas constraints belum terpenuhi, retry
  [Skip] sync-tugas constraints belum terpenuhi, retry

=== Hasil ===
  WorkResult(id=upload-foto-1, success=true, attempts=1)
  WorkResult(id=compress-cache, success=true, attempts=1)

=== State sistem berubah: charging=true ===
  [Enqueue] sync-tugas (Periodic) constraints=WorkConstraints(requiresNetwork=true, requiresCharging=true, requiresIdle=false)
  [Run] sync-tugas success=true attempts=1
  WorkResult(id=sync-tugas, success=true, attempts=1)

Perhatikan bahwa sync-tugas yang membutuhkan requiresCharging = true di-skip ketika sistem tidak charging. Worker tersebut di-enqueue ulang dan akhirnya berjalan ketika sistem state berubah menjadi charging.

Referensi Lanjutan #

  • Background work — panduan resmi opsi background work Android termasuk WorkManager untuk tugas yang perlu dijamin selesai

Selanjutnya #

Setelah tugas latar belakang terjadwal, kita perlu memberi tahu pengguna lewat notifikasi. Bab Notifications akan membahas notification channel, NotificationManager, dan foreground service.