Capítulo 17: Tarefas em Segundo Plano com WorkManager e CoroutineWorker

Especialização em Mobile Nativo com Android • Kotlin 2.0 & Jetpack Compose


🗺️ Mapa Conceitual do Agendamento Resiliente com WorkManager

flowchart TD
    APP["App / ViewModel (Dispara Agendamento)"] --> REQ["WorkRequest (OneTime ou PeriodicWorkRequest)"]

    subgraph RESTRICTIONS["Definição de Restrições (Constraints)"]
        REQ --> C1["NetworkType.CONNECTED / UNMETERED (Apenas Wi-Fi)"]
        REQ --> C2["setRequiresBatteryNotLow(true)"]
        REQ --> C3["setRequiresDeviceIdle(true)"]
    end

    subgraph WORK_MANAGER["Motor do WorkManager (AndroidX Architecture)"]
        RESTRICTIONS --> ENQUEUE["WorkManager.enqueueUniqueWork()"]
        ENQUEUE --> ROOM_INTERN["SQLite Interno do WorkManager (Garante Persistência)"]
        ROOM_INTERN --> JOB_SCHED["Android JobScheduler / AlarmManager (Compatibilidade Multi-API)"]
    end

    subgraph EXECUTION["Execução Segura no SO (Doze Mode Compliant)"]
        JOB_SCHED --> TRIGGER["Gatilho: Todas as Constraints são satisfeitas pelo SO"]
        TRIGGER --> WORKER["CoroutineWorker (doWork() com Suspensão Nativa)"]
        WORKER --> RESULT["Result.success() / Result.retry() / Result.failure()"]
    end

    RESULT -->|"Em caso de Retry"| BACKOFF["Exponential Backoff (Paupa Bateria)"]
    BACKOFF --> TRIGGER

    style APP fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style REQ fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style WORK_MANAGER fill:#b3e5fc,stroke:#0288d1,stroke-width:2px
    style WORKER fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style BACKOFF fill:#fce4ec,stroke:#e91e63,stroke-width:2px

🏛️ 1. O Desafio do Segundo Plano e a Política de Bateria do Android

Desde o Android 6.0 (Marshmallow), o sistema operacional introduziu mecanismos rigorosos de economia de energia conhecidos como Doze Mode e App Standby Buckets. Esses mecanismos desligam conexões de rádio (Wi-Fi/4G) e suspendem a CPU quando o aparelho repousa sobre uma mesa ou fica com a tela desligada. Tentar executar serviços contínuos em segundo plano (Service em background) resulta no término forçado do processo pelo sistema.

O Jetpack WorkManager é a solução oficial definitiva para processamento em segundo plano adiável e garantido:

  1. Garantia de Execução Pós-Reboot: O WorkManager salva as solicitações de trabalho em um banco de dados SQLite interno antes de repassar ao agendador nativo do sistema (JobScheduler). Mesmo que a bateria do celular acabe ou o usuário reinicie o aparelho, o trabalho continua registrado e será executado assim que o dispositivo for ligado novamente.
  2. Execução Condicional Inteligente (Constraints): O desenvolvedor pode especificar que uma rotina pesada (como upload de backup ou sincronização de banco de dados) só seja iniciada se o celular estiver conectado a uma rede Wi-Fi não-medida, conectado ao carregador e com a bateria acima do nível crítico.
  3. Integração com Coroutines via CoroutineWorker: A classe base CoroutineWorker executa o método doWork() em um contexto de suspensão nativo gerenciado em Dispatchers.Default ou Dispatchers.IO, garantindo cancelamento cooperativo e suporte a Result.retry() com algoritmo de recuo exponencial (Exponential Backoff).

📊 2. Comparativo de Mecanismos de Segundo Plano no Android

Mecanismo Garantido Pós-Reboot? Respeita Doze Mode? Caso de Uso Canônico
Kotlin Coroutines (viewModelScope) ❌ Não Não se aplica Chamadas imediatas enquanto a tela do usuário está visível
Foreground Service (com Notificação Fixa) ⚠️ Parcial Sim Rastreamento GPS contínuo ou reprodução de áudio ativo
AlarmManager Exato ❌ Não (sem reset) Quebra o Doze Mode Despertadores e alarmes que exigem disparo no milissegundo exato
WorkManager (com CoroutineWorker) ✅ Sim ✅ Sim Sincronização offline, uploads em lote, relatórios e auditorias

💻 3. Laboratório Prático: Worker de Sincronização com Constraints e Backoff

O código a seguir apresenta a criação de um CoroutineWorker para sincronização periódica de logs com tratamento de retry resiliente:

package br.com.portal.mobile.android.cap17

import android.content.Context
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import kotlinx.coroutines.delay
import java.util.concurrent.TimeUnit

// 1. Worker com Suspensão Nativa
class SincronizacaoLogsWorker(
    appContext: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {

    override suspend fun doWork(): Result {
        return try {
            // Simula leitura de pendências locais e upload HTTP
            delay(1500)

            val registrosEnviados = 120
            val dadosSaida = workDataOf("TOTAL_ENVIADO" to registrosEnviados)

            Result.success(dadosSaida)
        } catch (e: Exception) {
            // Retorna retry com Exponential Backoff em caso de falha de conexão
            Result.retry()
        }
    }
}

// 2. Agendamento com Restrições no WorkManager
fun agendarSincronizacaoLogs(context: Context) {
    // Restrições de Hardware e Conectividade
    val restricoes = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.UNMETERED) // Apenas em rede Wi-Fi gratuita
        .setRequiresBatteryNotLow(true) // Impede execução se bateria estiver baixa
        .build()

    // Requisição periódica a cada 6 horas com política de recuo exponencial
    val requisicaoTrabalho = PeriodicWorkRequestBuilder<SincronizacaoLogsWorker>(
        repeatInterval = 6,
        repeatIntervalTimeUnit = TimeUnit.HOURS
    )
        .setConstraints(restricoes)
        .setBackoffCriteria(
            backoffPolicy = BackoffPolicy.EXPONENTIAL,
            backoffDelay = 15,
            timeUnit = TimeUnit.MINUTES
        )
        .build()

    // Garante que exista apenas uma instância desse trabalho agendada (UniqueWork)
    WorkManager.getInstance(context).enqueueUniquePeriodicWork(
        "sincronizacao_periodica_logs",
        ExistingPeriodicWorkPolicy.KEEP,
        requisicaoTrabalho
    )
}

⚠️ 4. Armadilhas Comuns no Uso do WorkManager

  1. Agendar Intervalos Menores que 15 Minutos: O Android impõe um limite rígido de segurança para o PeriodicWorkRequest: o intervalo mínimo entre repetições é de 15 minutos. Tentar definir 5 minutos fará o WorkManager ignorar silenciosamente e arredondar para 15 minutos.
  2. Passar Objetos Grandes no Data de Entrada/Saída: O objeto Data do WorkManager possui um limite rígido de tamanho de 10 KB. Tentar trafegar bitmaps, áudios ou listas pesadas de JSON dispara uma IllegalStateException. Salve os arquivos em disco ou no Room e passe apenas o caminho (URI) ou identificador (ID).
  3. Esquecer de Usar ExistingWorkPolicy: Chamar enqueue() repetidamente sem uma política única (como KEEP ou REPLACE) pode criar centenas de workers concorrentes repetidos toda vez que o usuário abrir o app, esgotando recursos do aparelho.