🤖 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:
- 🎓 Trilha Essencial completa: Projetos P01-P10 (Cap 01-22)
- 🏗️ Projeto anterior: P14: Portais Dimensionais
🎯 Objetivo do Projeto
Implementar o agendamento persistente em background contendo:
- Criador de Workers: Declarar a classe
CoroutineWorkermanipulando códigos em threads secundárias. - Declaração de Restrições: Configurar regras de disparo baseadas em hardware (Wi-Fi e Tomada) com a classe
Constraints. - Estruturação de Requisições: Criar WorkRequests do tipo única (
OneTimeWorkRequest) ou periódica (PeriodicWorkRequest). - Status de Execução: Monitorar e reagir didaticamente à fila de execução do sistema operacional.
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
- WorkManager: A API padrão e recomendada do Android Jetpack para agendar tarefas em segundo plano persistentes e garantidas. Substitui os antigos
IntentServiceeJobScheduler. - Worker / CoroutineWorker: A classe onde você define a tarefa pesada a ser realizada. O método
doWork()é chamado de forma assíncrona longe da Thread principal (UI Thread). - Constraints: Um conjunto de exigências físicas impostas ao smartphone para que a tarefa possa rodar, evitando drenar a bateria e o plano de dados móveis 4G do usuário.
- OneTimeWorkRequest: Um comando de execução única agendado para rodar assim que as restrições forem atendidas.
- WorkManager.getInstance(context).enqueue(): Linha que entrega a tarefa para ser gerenciada pelo sistema operacional Android de forma oficial.
🛠️ Passo 1: Configurando as Dependências do WorkManager (Gradle)
- 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)
- 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:
- Notificação de Conclusão: dispare uma notificação local quando o
Workerterminar a sincronização com sucesso. - Política de Retry: configure um
BackoffPolicynoWorkRequeste simule uma falha para observar as tentativas automáticas.
📖 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)
}
}
}
}