Android WorkManager nel 2026: Task in Background, Constraints e Domande da Colloquio

Guida completa ad Android WorkManager 2.11/2.12 con esempi di codice Kotlin, configurazione dei constraints, concatenazione di richieste di lavoro e domande frequenti nei colloqui tecnici.

Tutorial Android WorkManager Background Tasks 2026

Android WorkManager gestisce lavori in background differibili e garantiti che persistono attraverso riavvii dell'app e del dispositivo. Rilasciato come parte di Android Jetpack, WorkManager 2.11.2 (stabile, marzo 2026) fornisce un'API unificata che funziona su API 23+ scegliendo automaticamente la migliore implementazione sottostante (JobScheduler, AlarmManager o Firebase JobDispatcher) in base al livello API del dispositivo.

Quando usare WorkManager

WorkManager è indicato per task che necessitano di esecuzione garantita: caricamento di log, sincronizzazione dati, elaborazione immagini. Per lavori immediati che non sopravvivono alla morte del processo, si utilizzano invece le Kotlin Coroutines.

Configurare WorkManager 2.11 in un progetto Kotlin

Prima di scrivere qualsiasi worker, è necessario dichiarare la dipendenza. WorkManager 2.11+ richiede minSdk 23 e compileSdk 33.

build.gradle.kts (app module)gradle
dependencies {
    val workVersion = "2.11.2"
    implementation("androidx.work:work-runtime-ktx:$workVersion")
    // Optional: for testing
    androidTestImplementation("androidx.work:work-testing:$workVersion")
}

L'artefatto work-runtime-ktx include estensioni Kotlin e supporto per Coroutine attraverso CoroutineWorker. Non è necessaria alcuna configurazione aggiuntiva per l'utilizzo base: WorkManager si inizializza automaticamente tramite un ContentProvider.

Creare un Worker semplice con doWork()

Una classe Worker sovrascrive doWork() e restituisce un Result. Il sistema esegue questo metodo su un thread in background.

SyncWorker.ktkotlin
class SyncWorker(
    context: Context,
    params: WorkerParameters
) : Worker(context, params) {

    override fun doWork(): Result {
        // Read input data
        val userId = inputData.getString("user_id") ?: return Result.failure()
        
        return try {
            // Perform sync operation
            val syncService = SyncService.getInstance(applicationContext)
            syncService.syncUserData(userId)
            Result.success()
        } catch (e: IOException) {
            // Retry on network errors
            Result.retry()
        } catch (e: Exception) {
            // Permanent failure
            Result.failure()
        }
    }
}

Esistono tre possibili risultati: Result.success() indica il completamento, Result.failure() interrompe i tentativi, e Result.retry() ripianifica secondo la policy di backoff.

Usare CoroutineWorker per funzioni Suspend

Quando il lavoro coinvolge funzioni suspend (chiamate Retrofit, query Room), CoroutineWorker elimina l'annidamento dei callback.

UploadWorker.ktkotlin
class UploadWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        val imageUri = inputData.getString("image_uri")
            ?: return Result.failure()
        
        // Progress reporting (visible in WorkInfo observers)
        setProgress(workDataOf("status" to "compressing"))
        
        val compressed = ImageCompressor.compress(imageUri)
        
        setProgress(workDataOf("status" to "uploading"))
        
        return try {
            val response = ApiClient.imageService.upload(compressed)
            val outputData = workDataOf("url" to response.imageUrl)
            Result.success(outputData)
        } catch (e: HttpException) {
            if (e.code() in 500..599) Result.retry()
            else Result.failure()
        }
    }
}

CoroutineWorker.doWork() viene eseguito su Dispatchers.Default. Per cambiare dispatcher, si utilizza withContext() all'interno della funzione.

Definire Constraints per l'esecuzione condizionale

I constraints impediscono l'esecuzione del lavoro finché le condizioni non sono soddisfatte. Questo risparmia batteria ed evita tentativi falliti.

ScheduleUpload.ktkotlin
fun scheduleUpload(context: Context, imageUri: String) {
    val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.UNMETERED) // WiFi only
        .setRequiresBatteryNotLow(true)
        .setRequiresStorageNotLow(true)
        .build()

    val uploadRequest = OneTimeWorkRequestBuilder<UploadWorker>()
        .setConstraints(constraints)
        .setInputData(workDataOf("image_uri" to imageUri))
        .setBackoffCriteria(
            BackoffPolicy.EXPONENTIAL,
            Duration.ofMinutes(1)
        )
        .addTag("upload")
        .build()

    WorkManager.getInstance(context)
        .enqueueUniqueWork(
            "upload_$imageUri",
            ExistingWorkPolicy.KEEP,
            uploadRequest
        )
}

I constraints disponibili includono tipo di rete (CONNECTED, UNMETERED, METERED, NOT_ROAMING), livello batteria, stato di ricarica, spazio di archiviazione e stato idle del dispositivo. WorkManager 2.10+ accetta anche un NetworkRequest raw per un controllo di rete più granulare.

Insight da colloquio

Una domanda frequente nei colloqui riguarda quando usare KEEP vs REPLACE in enqueueUniqueWork. KEEP ignora nuove richieste se esiste già lavoro con lo stesso nome, REPLACE cancella il lavoro esistente e ricomincia. Si usa KEEP per upload dove i duplicati sprecano banda, REPLACE per sincronizzazioni dove contano solo i dati più recenti.

Concatenare richieste di lavoro con then() e combine()

Workflow complessi richiedono esecuzione sequenziale e parallela. WorkManager concatena le richieste di lavoro usando beginWith() e then().

WorkChain.ktkotlin
fun processAndUploadImages(context: Context, imageUris: List<String>) {
    val workManager = WorkManager.getInstance(context)

    // Parallel compression workers
    val compressRequests = imageUris.map { uri ->
        OneTimeWorkRequestBuilder<CompressWorker>()
            .setInputData(workDataOf("uri" to uri))
            .build()
    }

    // Single upload worker runs after all compressions complete
    val uploadRequest = OneTimeWorkRequestBuilder<BatchUploadWorker>()
        .setConstraints(
            Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build()
        )
        .build()

    // Cleanup runs after upload, regardless of success
    val cleanupRequest = OneTimeWorkRequestBuilder<CleanupWorker>()
        .build()

    workManager
        .beginWith(compressRequests) // Parallel
        .then(uploadRequest)          // Sequential
        .then(cleanupRequest)          // Sequential
        .enqueue()
}

L'output dei worker paralleli viene unito in un ArrayCreatingInputMerger di default. Il worker successivo riceve tutte le coppie chiave-valore, con array creati per chiavi duplicate.

Pronto a superare i tuoi colloqui su Android?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Pianificare lavoro periodico con PeriodicWorkRequest

PeriodicWorkRequest esegue ripetutamente con un intervallo minimo di 15 minuti (Android impone questo limite).

PeriodicSync.ktkotlin
fun scheduleDailySync(context: Context) {
    val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
        repeatInterval = 6, 
        repeatIntervalTimeUnit = TimeUnit.HOURS,
        flexTimeWindow = 30,
        flexTimeUnit = TimeUnit.MINUTES
    )
        .setConstraints(
            Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build()
        )
        .addTag("periodic_sync")
        .build()

    WorkManager.getInstance(context)
        .enqueueUniquePeriodicWork(
            "daily_sync",
            ExistingPeriodicWorkPolicy.UPDATE,
            syncRequest
        )
}

La finestra flex consente a WorkManager di raggruppare il lavoro con altri job, migliorando l'efficienza della batteria. Un intervallo di 6 ore con flex di 30 minuti significa che l'esecuzione avviene tra le 5:30 e le 6:00 dopo l'esecuzione precedente.

Osservare lo stato del lavoro con LiveData e Flow

WorkManager espone lo stato del lavoro attraverso WorkInfo. L'osservazione avviene usando LiveData o Kotlin Flow.

WorkObserver.ktkotlin
class UploadViewModel(application: Application) : AndroidViewModel(application) {

    private val workManager = WorkManager.getInstance(application)

    // Flow-based observation
    fun observeUpload(workId: UUID): Flow<WorkInfo?> {
        return workManager.getWorkInfoByIdFlow(workId)
    }

    // Check if any upload is running
    val activeUploads: Flow<List<WorkInfo>> = 
        workManager.getWorkInfosByTagFlow("upload")
            .map { workInfos ->
                workInfos.filter { it.state == WorkInfo.State.RUNNING }
            }
}

// In Compose UI
@Composable
fun UploadProgress(workId: UUID, viewModel: UploadViewModel) {
    val workInfo by viewModel.observeUpload(workId)
        .collectAsState(initial = null)

    when (workInfo?.state) {
        WorkInfo.State.RUNNING -> {
            val status = workInfo?.progress?.getString("status") ?: "working"
            CircularProgressIndicator()
            Text(status)
        }
        WorkInfo.State.SUCCEEDED -> {
            val url = workInfo?.outputData?.getString("url")
            Text("Uploaded: $url")
        }
        WorkInfo.State.FAILED -> Text("Upload failed")
        else -> {}
    }
}

Gli stati del lavoro seguono un ciclo di vita: ENQUEUED, RUNNING, SUCCEEDED/FAILED/CANCELLED, BLOCKED (in attesa di dipendenze).

WorkManager 2.12 Work Metrics API

WorkManager 2.12.0-rc01 (agosto 2026) introduce WorkMetricsInfo per tracciare lo storico delle esecuzioni.

MetricsExample.ktkotlin
class MetricsRepository(private val context: Context) {
    
    private val workManager = WorkManager.getInstance(context)

    suspend fun getWorkerMetrics(workerName: String): List<WorkMetricsInfo> {
        val query = WorkMetricsQuery.Builder()
            .setWorkerClassName(workerName)
            .setLimit(100)
            .build()
        
        return workManager.getWorkMetrics(query)
    }

    fun analyzeFailures(metrics: List<WorkMetricsInfo>): Map<Int, Int> {
        // Count stop reasons
        return metrics
            .flatMap { it.stopReasonCounts.entries }
            .groupBy { it.key }
            .mapValues { entry -> entry.value.sumOf { it.value } }
    }
}

Le metriche includono durata dell'esecuzione, conteggio dei retry, motivi di stop e timestamp. L'API permette di debuggare errori intermittenti in produzione senza infrastruttura di logging personalizzata.

Retention delle metriche

I dati di WorkMetricsInfo vengono eliminati dopo 7 giorni di default. La retention può essere configurata con Configuration.Builder().setWorkMetricsRetentionDuration().

Testare i Worker con WorkManagerTestInitHelper

I test unitari dei worker richiedono l'artefatto work-testing. TestListenableWorkerBuilder crea worker senza accodarli.

SyncWorkerTest.ktkotlin
@RunWith(AndroidJUnit4::class)
class SyncWorkerTest {

    private lateinit var context: Context

    @Before
    fun setup() {
        context = ApplicationProvider.getApplicationContext()
        val config = Configuration.Builder()
            .setMinimumLoggingLevel(Log.DEBUG)
            .setExecutor(SynchronousExecutor())
            .build()
        WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
    }

    @Test
    fun syncWorker_withValidUserId_succeeds() = runTest {
        val inputData = workDataOf("user_id" to "123")
        
        val worker = TestListenableWorkerBuilder<SyncWorker>(context)
            .setInputData(inputData)
            .build()

        val result = worker.doWork()
        
        assertThat(result).isEqualTo(ListenableWorker.Result.success())
    }

    @Test
    fun syncWorker_withoutUserId_fails() = runTest {
        val worker = TestListenableWorkerBuilder<SyncWorker>(context)
            .build()

        val result = worker.doWork()
        
        assertThat(result).isEqualTo(ListenableWorker.Result.failure())
    }
}

Per i test di integrazione che verificano constraints e concatenazione, si usa TestDriver per simulare il soddisfacimento dei constraints e il passaggio del tempo.

Domande frequenti nei colloqui su WorkManager

I colloqui tecnici testano frequentemente la comprensione delle garanzie e dei trade-off di WorkManager. Queste domande appaiono nei colloqui Android di livello mid e senior.

D: Cosa succede a un WorkRequest quando l'app viene terminata?

WorkManager persiste le richieste di lavoro in un database Room. Quando l'app si riavvia o il sistema segnala il soddisfacimento dei constraints, il lavoro in sospeso riprende. Questa garanzia distingue WorkManager dal lavoro in CoroutineScope che muore con il processo.

D: Come si differenzia WorkManager da AlarmManager?

AlarmManager pianifica allarmi a tempo esatto ed esegue codice a orari specifici. WorkManager pianifica lavoro differibile che viene eseguito quando i constraints sono soddisfatti, senza garanzia di timing esatto. Si usa AlarmManager per allarmi rivolti all'utente, WorkManager per l'elaborazione dati in background.

D: WorkManager può eseguire lavoro immediatamente?

Sì, con setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST). Le richieste di lavoro expedited usano lo slot del foreground service su API 31+ o JobScheduler.setImportantWhileForeground() su API precedenti. Si applicano limiti di quota, quindi OutOfQuotaPolicy definisce il comportamento di fallback.

D: Come si cancellerebbero tutti gli upload in sospeso?

kotlin
WorkManager.getInstance(context).cancelAllWorkByTag("upload")

I tag abilitano operazioni massive. In alternativa, cancelUniqueWork("name") cancella una catena specifica.

Gestire gli stop indotti dal sistema con policy di Backoff

Quando il sistema ferma un worker (ottimizzazione batteria, pressione di memoria), la policy di backoff determina il timing dei retry.

BackoffConfig.ktkotlin
val request = OneTimeWorkRequestBuilder<UploadWorker>()
    .setBackoffCriteria(
        BackoffPolicy.EXPONENTIAL,
        Duration.ofSeconds(30) // Initial delay, minimum 10 seconds
    )
    .setBackoffOnSystemInterruption(true) // New in 2.11
    .build()

Il backoff esponenziale raddoppia il ritardo ad ogni retry: 30s, 60s, 120s, fino a un massimo di 5 ore. Il backoff lineare aggiunge il ritardo iniziale ogni volta: 30s, 60s, 90s. Il nuovo flag setBackoffOnSystemInterruption() in WorkManager 2.11 applica il backoff anche quando il sistema (non il worker) ha causato lo stop.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Punti chiave per l'uso di WorkManager in produzione

  • Preferire CoroutineWorker a Worker quando si chiamano funzioni suspend. Evita il blocco dei thread e si integra con il codice coroutine esistente.
  • Usare enqueueUniqueWork() per prevenire lavoro duplicato. Scegliere KEEP per operazioni idempotenti, REPLACE quando conta solo l'ultima richiesta.
  • Impostare tag significativi su ogni richiesta. I tag abilitano osservazione, cancellazione e debugging attraverso le catene di lavoro.
  • Testare i worker in isolamento con TestListenableWorkerBuilder, poi testare le catene con simulazione di constraints tramite TestDriver.
  • L'API WorkMetricsInfo di WorkManager 2.12 fornisce lo storico delle esecuzioni. Va usata per debuggare pattern di retry e motivi di stop in produzione.
  • I constraints risparmiano batteria. Un worker che fallisce per mancanza di rete spreca cicli CPU e consuma energia nei tentativi successivi.
  • Il minimo di 15 minuti per il lavoro periodico è imposto da Android, non da WorkManager. I task periodici vanno progettati per tollerare questo intervallo.
Sfida del giorno

Sapresti trovare il bug in Android?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore di SharpSkill

Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.

Aggiornato il 22 agosto 2026

Tag

#android
#workmanager
#kotlin
#background-tasks
#jetpack

Condividi

Articoli correlati