🤖 P15: J.A.R.V.I.S. Sync (Tarefas em Segundo Plano com WorkManager)

Bem-vindo à Fase 8: Ciclos de Vida & Execução em Background! 🤖

Neste projeto de engenharia de software avançada, você aprenderá a delegar tarefas de processamento pesado em segundo plano que persistem mesmo após o fechamento do app e reinicialização do celular, utilizando a biblioteca oficial WorkManager do Google. Criaremos o painel de upload de arquivos das indústrias Stark liderado pelo mordomo virtual J.A.R.V.I.S. de Homem de Ferro, agendando uma rotina que só é disparada se o dispositivo estiver fisicamente conectado ao carregador e no Wi-Fi.


✅ Pré-requisitos

Antes de começar, certifique-se de já ter estudado:


🎯 Objetivo do Projeto

Implementar o agendamento persistente em background contendo:

graph TD
    A[Usuário clica em Ativar Protocolo] --> B[Criar Constraints: Wi-Fi + Tomada]
    B --> C[Construir OneTimeWorkRequest associado ao DatabaseBackupWorker]
    C --> D[WorkManager.enqueue enfileira tarefa no SO]
    D --> E{Celular atende restrições físicas?}
    E -- Não --> F[Aguardar em fila indeterminadamente - mesmo se app fechar]
    E -- Sim --> G[SO acorda e executa doWork() em Thread secundária]
    G --> H[Retorna Result.success() & Conclui tarefa]

📖 Dicionário do Projeto


🛠️ Passo 1: Configurando as Dependências do WorkManager (Gradle)

  1. Abra o arquivo build.gradle (Module :app) e inclua a biblioteca oficial do WorkManager:
    dependencies {
    // Jetpack WorkManager (Kotlin + Coroutines)
    implementation "androidx.work:work-runtime-kotlin:2.8.1"
    ...
    }
    

    Clique em Sync Now.


🛠️ Passo 2: Criando a Classe do Worker (DatabaseBackupWorker.kt)

  1. Crie a classe concreta que encapsulará a sua tarefa assíncrona em background: ```kotlin package br.com.curso.jarvis

import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters

class DatabaseBackupWorker( context: Context, workerParams: WorkerParameters ) : CoroutineWorker(context, workerParams) {

override suspend fun doWork(): Result {
    return try {
        // Insira aqui o seu código assíncrono (ex: salvar em banco local e subir para servidor)
        kotlinx.coroutines.delay(3000) // Simulação de 3 segundos de upload pesado
        Result.success()
    } catch (e: Exception) {
        Result.retry() // Caso falhe a rede, reagenda a tentativa automaticamente
    }
} } ```

🧠 Passo 3: Configurando as Constraints no Compose

Configuramos o agendamento de forma reativa no clique do botão na tela da MainActivity.kt, garantindo segurança e aderência às melhores práticas de consumo de energia do ecossistema Android:

// 1. Declarar as Restrições
val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.UNMETERED) // Apenas no Wi-Fi
    .setRequiresCharging(true) // Apenas se estiver carregando
    .build()

// 2. Criar a Requisição de Trabalho Único
val backupRequest = OneTimeWorkRequestBuilder<DatabaseBackupWorker>()
    .setConstraints(constraints)
    .build()

// 3. Enfileirar no Sistema Operacional
WorkManager.getInstance(context).enqueue(backupRequest)

🏆 Desafios para você (Upgrade!)

Se você terminou de programar a funcionalidade base e tudo está funcionando, experimente estes upgrades:


📖 Gabarito Oficial de Código (Para Conferência)

Use os códigos Kotlin completos abaixo para validar sua implementação.

📄 Código de Lógica e Tela Completa (MainActivity.kt)

Disponível em: app/src/main/java/br/com/curso/jarvis/MainActivity.kt

package br.com.curso.jarvis

import android.content.Context
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.work.*
import java.util.concurrent.TimeUnit

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            JarvisTheme {
                JarvisScreen()
            }
        }
    }
}

@Composable
fun JarvisTheme(content: @Composable () -> Unit) {
    MaterialTheme(
        colorScheme = darkColorScheme(
            primary = Color(0xFF00E5FF), // Azul Jarvis
            background = Color(0xFF080D14),
            surface = Color(0xFF101924)
        ),
        content = content
    )
}

// 1. WORKER CLASSE PARA TAREFAS DE BACKGROUND
class DatabaseBackupWorker(
    context: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
    
    override suspend fun doWork(): Result {
        // Simula upload de dados para o servidor de backup das indústrias Stark
        return try {
            kotlinx.coroutines.delay(3000) // Simula 3 segundos de upload
            Result.success()
        } catch (e: Exception) {
            Result.retry()
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun JarvisScreen() {
    val context = LocalContext.current
    var syncStatus by remember { mutableStateOf("Aguardando Inicialização") }
    var isLoading by remember { mutableStateOf(false) }

    Scaffold(
        topBar = {
            SmallTopAppBar(
                title = { Text("J.A.R.V.I.S. BACKUP SYSTEM", fontWeight = FontWeight.Bold, letterSpacing = 2.sp) },
                colors = TopAppBarDefaults.smallTopAppBarColors(
                    containerColor = Color(0xFF101924),
                    titleContentColor = Color(0xFF00E5FF)
                )
            )
        }
    ) { padding ->
        Column(
            modifier = Modifier
                .padding(padding)
                .fillMaxSize()
                .background(Color(0xFF080D14))
                .padding(24.dp),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Box(
                modifier = Modifier
                    .size(140.dp)
                    .background(Color(0xFF00E5FF).copy(alpha = 0.05f), shape = RoundedCornerShape(70.dp)),
                contentAlignment = Alignment.Center
            ) {
                if (isLoading) {
                    CircularProgressIndicator(color = Color(0xFF00E5FF), modifier = Modifier.size(80.dp))
                } else {
                    Text("🤖", fontSize = 64.sp)
                }
            }

            Spacer(modifier = Modifier.height(32.dp))

            Card(
                modifier = Modifier.fillMaxWidth(),
                colors = CardDefaults.cardColors(containerColor = Color(0xFF101924)),
                shape = RoundedCornerShape(16.dp)
            ) {
                Column(modifier = Modifier.padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally) {
                    Text("STATUS DO PROTOCOLO STARK", fontSize = 12.sp, color = Color.Gray)
                    Spacer(modifier = Modifier.height(8.dp))
                    Text(syncStatus, fontSize = 18.sp, fontWeight = FontWeight.Bold, color = Color.White)
                }
            }

            Spacer(modifier = Modifier.height(40.dp))

            Button(
                onClick = {
                    isLoading = true
                    syncStatus = "Analisando restrições..."

                    // 2. CONFIGURAR RESTRIÇÕES DE HARDWARE (CRÍTICO!)
                    val constraints = Constraints.Builder()
                        .setRequiredNetworkType(NetworkType.UNMETERED) // Apenas no Wi-Fi
                        .setRequiresCharging(true) // Apenas carregando na tomada
                        .build()

                    // 3. REGISTRAR WORKREQUEST
                    val backupRequest = OneTimeWorkRequestBuilder<DatabaseBackupWorker>()
                        .setConstraints(constraints)
                        .build()

                    WorkManager.getInstance(context).enqueue(backupRequest)
                    
                    // Simular status didático para visualização de restrições do SO
                    syncStatus = "Tarefa enfileirada no WorkManager! Aguardando celular conectar ao Wi-Fi e Tomada para enviar o backup."
                    isLoading = false
                },
                colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF00E5FF))
            ) {
                Text("ATIVAR PROTOCOLO BACKUP J.A.R.V.I.S.", color = Color.Black, fontWeight = FontWeight.Bold)
            }
        }
    }
}