📚 Pré-requisitos Teóricos: este projeto é a evolução direta da Parte 04 — Mobile Android Clássico (Java + XML Views) e consome os serviços da Parte 01 — Spring Boot API.
Seja muito bem-vindo à Parte 10 do ecossistema de aplicações Lista de Tarefas!
Na Parte 04, você construiu um cliente móvel nativo utilizando o ecossistema clássico do Android: Java puro, layouts estáticos em XML, infladores de view com findViewById, o gerenciador de memória RecyclerView com seus adapters e viewholders verbosos, e callbacks assíncronos de rede. Aquele projeto cumpriu um papel pedagógico vital: compreender como funcionam as entranhas da plataforma Android desde seus primórdios.
Agora, na Parte 10, você dará o salto profissional definitivo na engenharia de software para dispositivos móveis: aprenderá como e por que a indústria migrou do modelo imperativo de Views XML para o paradigma declarativo e reativo do Jetpack Compose com Kotlin moderno.
\[\text{Android Clássico (Java + Views XML + RecyclerView)} \longrightarrow \text{Android Moderno (Kotlin + Jetpack Compose + UDF)}\]💡 “Construa interfaces nativas modernas com funções declarativas puras em Kotlin e gerenciamento unidirecional de estado.”
Trilha Prática de Aplicações Full Stack (10 Partes Integradas):
├── Parte 01: API REST Central com Spring Boot + YARC + Thymeleaf (SSR)
├── Parte 02: Cliente Web Desacoplado com Svelte (JavaScript Puro)
├── Parte 03: Cliente Desktop Clássico com Java Swing (UI Imperativa)
├── Parte 04: Cliente Mobile Clássico com Android (Java + Views XML)
├── Parte 05: Arquitetura Corporativa, DevOps e Docker + PostgreSQL 16
├── Parte 06: Web ➔ Mobile Híbrido: Vue.js 3 ➔ Quasar Framework
├── Parte 07: Web ➔ Mobile Cross-Platform: React 19 ➔ React Native (Expo)
├── Parte 08: Web ➔ Mobile Híbrido: Angular Standalone ➔ Ionic Framework
├── Parte 09: Evolução Arquitetural Desktop: De Swing para JavaFX Moderno (MVC + CSS)
└── Parte 10: Evolução Arquitetural Mobile: De Java/XML para Kotlin + Jetpack Compose [ESTE PROJETO]
Compreender o contraponto entre o desenvolvimento clássico baseado em Views e o desenvolvimento contemporâneo com Compose é uma das competências mais exigidas em entrevistas técnicas de alto nível:
| Dimensão de Engenharia | Android Clássico (Parte 04) | Android Moderno com Compose (Parte 10) | Benefício de Engenharia |
|---|---|---|---|
| Paradigma de UI | Imperativo: Infla arquivos XML e manipula nós da árvore manualmente (findViewById, setText, setVisibility). |
Declarativo: A interface é descrita como uma função pura em Kotlin (@Composable fun TarefaScreen(...)). A UI reage ao estado. |
Elimina descompasso entre o estado real dos dados e os textos visíveis na tela. |
| Listagem de Itens | RecyclerView com TarefaAdapter, ViewHolder, onCreateViewHolder, onBindViewHolder e DiffUtil (mais de 120 linhas). |
LazyColumn { items(tarefas, key = { it.id }) { TarefaCard(it) } } (menos de 10 linhas). |
Redução de mais de 85% do código boilerplate sem perder reciclagem de memória. |
| Linguagem & Tipagem | Java 17/21: Verbosidade clássica, getters/setters e risco constante de exceções em tempo de execução (NullPointerException). |
Kotlin 2.0: Classes concisas (data class Tarefa), imutabilidade por padrão e Null-Safety validada em tempo de compilação. |
Código expressivo, limpo e à prova de travamentos por valores nulos. |
| Assincronismo de Rede | Callbacks aninhados do Retrofit (call.enqueue(new Callback() { ... })) ou threads manuais com runOnUiThread. |
Kotlin Coroutines (suspend fun): O código assíncrono é escrito de forma sequencial com withContext(Dispatchers.IO). |
Código legível, sem “callback hell”, com cancelamento estruturado e livre de vazamento de memória. |
| Gerenciamento de Estado | Estado fragmentado entre Activity, Adapter e XML; perda de dados em rotações de tela sem onSaveInstanceState. |
UDF (Unidirectional Data Flow): ViewModel sobrevive à rotação e expõe um StateFlow<TarefaUiState> imutável. |
Estado centralizado, previsível e facilmente testável com testes unitários JUnit puros. |
| Design System & Estilo | styles.xml, themes.xml, seletores em drawables XML e color.xml. |
Material Design 3 em Kotlin: Tokens tipados (MaterialTheme.colorScheme, animações nativas com animateColorAsState). |
Estilização tipada com suporte instantâneo a Dark Mode e Dynamic Colors sem tocar em XML. |
Assim como todos os demais clientes da série, o aplicativo Android Jetpack Compose conecta-se à nossa Fonte Única da Verdade (SSOT): a API REST Spring Boot rodando na porta 8080, padronizada sob o contrato canônico { id, descricao, concluida }.
flowchart TD
subgraph Clientes ["📱 Clientes Nativos do Ecossistema"]
C1["📱 Android Clássico (Parte 04)<br>Java + Views XML"]
C2["📱 Android Moderno (Parte 10)<br>Kotlin + Jetpack Compose"]
end
subgraph Rede ["🌐 Ponte de Virtualização"]
EMU["10.0.2.2:8080<br>(Gateway Emulador AVD)"]
WIFI["192.168.x.x:8080<br>(Smartphone Físico Wi-Fi)"]
end
subgraph Backend ["⚙️ Fonte Única da Verdade (SSOT)"]
API["🚀 Spring Boot REST API<br>fullstack_tarefas_01_spring_api<br>Porta 8080"]
DB[("🗄️ Banco de Dados H2 / PostgreSQL")]
end
C1 --> EMU
C2 --> EMU
C2 --> WIFI
EMU --> API
WIFI --> API
API --> DB
style C2 fill:#eff6ff,stroke:#3b82f6,stroke-width:2px
style API fill:#f0fdf4,stroke:#10b981,stroke-width:2px
10.0.2.2) vs. Aparelho Físicolocalhost ou 127.0.0.1 dentro dele refere-se ao próprio sistema Android emulado! Para atingir a porta 8080 do seu computador de desenvolvimento, o Retrofit é configurado com a URL canônica:
\(\mathbf{http://10.0.2.2:8080/api/}\)RetrofitClient.kt, substitui-se 10.0.2.2 pelo IP privado da máquina (ex: http://192.168.1.105:8080/api/).android:usesCleartextTraffic="true" no AndroidManifest.xml é obrigatória para que o sistema operacional não bloqueie as requisições.O Jetpack Compose adota estritamente o padrão Unidirectional Data Flow (UDF):
onToggle(tarefa.id)).TarefaViewModel intercepta o evento, dispara uma Coroutine no escopo viewModelScope.launch e aciona o repositório em segundo plano (Dispatchers.IO).MutableStateFlow interno emitindo uma nova instância de TarefaUiState.TarefaCard correspondente é recomposto; todo o restante da tela permanece intocado!| ID | Nome do Requisito | Descrição Detalhada | Endpoint REST |
|---|---|---|---|
| RF-C01 | Listagem em LazyColumn | Consultar tarefas da API via Coroutines e renderizar lista reativa sem adapters. | GET /api/tarefas |
| RF-C02 | Cadastro via Modal | Coletar texto no TarefaDialog, validar obrigatoriedade e despachar POST. |
POST /api/tarefas |
| RF-C03 | Alternância Atômica | Inverter booleano concluida com atualização otimista na UI e persistência no banco. |
PUT /api/tarefas/{id} |
| RF-C04 | Edição de Descrição | Carregar texto original no diálogo e disparar PUT com a nova descrição. |
PUT /api/tarefas/{id} |
| RF-C05 | Exclusão de Tarefa | Enviar DELETE /api/tarefas/{id} e remover o item correspondente do StateFlow. |
DELETE /api/tarefas/{id} |
| ID | Categoria | Critério Técnico |
|---|---|---|
| RNF-C01 | UI Declarativa Pura | 100% da interface construída em Jetpack Compose, sem arquivos de layout XML (res/layout/). |
| RNF-C02 | Concorrência com Coroutines | Nenhuma requisição de rede pode rodar na Thread Principal (Main Dispatcher). |
| RNF-C03 | Arquitetura MVVM & UDF | A camada de visão deve ser completamente desacoplada da camada de dados via StateFlow. |
| RNF-C04 | Design System Material 3 | Utilizar componentes oficiais do Material 3 com suporte a temas claro e escuro. |
| RNF-C05 | Segurança de Tráfego Local | O manifesto deve liberar tráfego HTTP em texto claro para comunicação com o emulador. |
flowchart LR
User(["👤 Usuário Móvel (Android AVD / Smartphone)"])
subgraph ComposeApp ["📱 Cliente Android (Kotlin + Jetpack Compose)"]
direction TB
UC01["UC01: Visualizar & Filtrar Tarefas em LazyColumn"]
UC02["UC02: Inserir Nova Tarefa via FAB"]
UC03["UC03: Alternar Status de Conclusão"]
UC04["UC04: Editar Tarefa em Diálogo Modal"]
UC05["UC05: Excluir Tarefa Permanentemente"]
end
subgraph SpringAPI ["⚙️ API REST Spring Boot (Porta 8080)"]
direction TB
E1["GET /api/tarefas"]
E2["POST /api/tarefas"]
E3["PUT /api/tarefas/{id}"]
E4["DELETE /api/tarefas/{id}"]
end
User --> UC01
User --> UC02
User --> UC03
User --> UC04
User --> UC05
UC01 -.->|"suspend fun listar()"| E1
UC02 -.->|"suspend fun criar()"| E2
UC03 -.->|"suspend fun atualizar()"| E3
UC04 -.->|"suspend fun atualizar()"| E3
UC05 -.->|"suspend fun deletar()"| E4
TarefaViewModel dispara automaticamente carregarTarefas(). A tela exibe o CircularProgressIndicator. Ao receber a lista do Retrofit, a LazyColumn renderiza os TarefaCard ordenados pelo ID decrescente. Ao tocar nos chips “Todas”, “Pendentes” ou “Concluídas”, a listagem é filtrada instantaneamente sem refazer requisições de rede.+ (FAB). O TarefaDialog surge na tela. O usuário digita “Estudar Jetpack Compose” e clica em “Salvar”. O ViewModel dispara POST /api/tarefas. A nova tarefa é retornada com o ID gerado pelo banco e é inserida no topo do estado imutável.PUT /api/tarefas/{id}. O servidor confirma e o estado se consolida.PUT /api/tarefas/{id}. O card é recomposto cirurgicamente com o novo texto.DELETE /api/tarefas/{id}. Com a confirmação 204 No Content do Spring Boot, o item é filtrado para fora do StateFlow e o card desaparece da tela com animação suave.| História de Usuário | Requisito Funcional | Requisito Não-Funcional | Caso de Uso | Componente Compose / Kotlin |
|---|---|---|---|---|
| US-C01 (Visualização Declarativa) | RF-C01 | RNF-C01, RNF-C02, RNF-C03 | UC01 | TarefaScreen.kt, FiltroSegmentedControl.kt |
| US-C02 (Inclusão via FAB) | RF-C02 | RNF-C01, RNF-C04 | UC02 | TarefaDialog.kt, TarefaViewModel.kt |
| US-C03 (Alternância de Status) | RF-C03 | RNF-C02, RNF-C03 | UC03 | TarefaCard.kt, TarefaRepository.kt |
| US-C04 (Edição Direta) | RF-C04 | RNF-C01, RNF-C03 | UC04 | TarefaDialog.kt, TarefaApiService.kt |
| US-C05 (Exclusão Segura) | RF-C05 | RNF-C02, RNF-C05 | UC05 | TarefaCard.kt, TarefaViewModel.kt |
A pasta do cliente móvel mobile-android-compose/ possui a seguinte organização arquitetural:
catalogo_projetos/fullstack_tarefas_10_spring_compose/
├── assets/
│ ├── arquitetura_android_compose_ecosystem.svg
│ ├── comparativo_views_xml_vs_compose.svg
│ └── fluxo_unidirecional_udf_compose_api.svg
├── index.md
├── listadetarefas-painel.ps1
└── mobile-android-compose/
├── build.gradle.kts
├── settings.gradle.kts
├── README.md
└── app/
├── build.gradle.kts
└── src/main/
├── AndroidManifest.xml
└── java/br/com/curso/listadetarefas/
├── MainActivity.kt
├── data/
│ ├── model/Tarefa.kt
│ ├── remote/
│ │ ├── TarefaApiService.kt
│ │ └── RetrofitClient.kt
│ └── repository/TarefaRepository.kt
└── ui/
├── theme/
│ ├── Color.kt
│ ├── Type.kt
│ └── Theme.kt
├── screens/
│ ├── TarefaUiState.kt
│ └── TarefaScreen.kt
├── viewmodel/TarefaViewModel.kt
└── components/
├── TarefaCard.kt
├── TarefaDialog.kt
└── FiltroSegmentedControl.kt
MainActivity.ktObserve a concisão extrema: a Activity tradicional cheia de findViewById e setContentView(R.layout.activity_main) é substituída por um bloco declarativo com suporte nativo a bordas de tela modernas (Edge-to-Edge):
package br.com.curso.listadetarefas
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import br.com.curso.listadetarefas.ui.screens.TarefaScreen
import br.com.curso.listadetarefas.ui.theme.TarefasTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
TarefasTheme {
TarefaScreen()
}
}
}
}
Tarefa.kt):package br.com.curso.listadetarefas.data.model
import com.google.gson.annotations.SerializedName
data class Tarefa(
@SerializedName("id") val id: Long? = null,
@SerializedName("descricao") val descricao: String,
@SerializedName("concluida") val concluida: Boolean = false
)
TarefaApiService.kt):package br.com.curso.listadetarefas.data.remote
import br.com.curso.listadetarefas.data.model.Tarefa
import retrofit2.Response
import retrofit2.http.*
interface TarefaApiService {
@GET("tarefas")
suspend fun listar(): List<Tarefa>
@POST("tarefas")
suspend fun criar(@Body tarefa: Tarefa): Tarefa
@PUT("tarefas/{id}")
suspend fun atualizar(@Path("id") id: Long, @Body tarefa: Tarefa): Tarefa
@DELETE("tarefas/{id}")
suspend fun deletar(@Path("id") id: Long): Response<Unit>
}
TarefaViewModel.ktO ViewModel orquestra todas as operações em segundo plano através do viewModelScope e expõe um único fluxo observável imutável (StateFlow):
class TarefaViewModel(
private val repository: TarefaRepository = TarefaRepository()
) : ViewModel() {
private val _uiState = MutableStateFlow(TarefaUiState(isLoading = true))
val uiState: StateFlow<TarefaUiState> = _uiState.asStateFlow()
init {
carregarTarefas()
}
fun carregarTarefas() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
repository.listarTarefas()
.onSuccess { lista ->
_uiState.update {
it.copy(tarefas = lista.sortedByDescending { t -> t.id ?: 0L }, isLoading = false)
}
}
.onFailure { erro ->
_uiState.update {
it.copy(isLoading = false, errorMessage = "Falha ao conectar: ${erro.message}")
}
}
}
}
fun alternarStatus(tarefa: Tarefa) {
val modificada = tarefa.copy(concluida = !tarefa.concluida)
viewModelScope.launch {
_uiState.update { estado ->
estado.copy(tarefas = estado.tarefas.map { if (it.id == tarefa.id) modificada else it })
}
repository.atualizarTarefa(modificada).onFailure { carregarTarefas() }
}
}
}
TarefaScreen.ktO componente substitui todas as Views e Adapters XML antigos utilizando o componente de lista moderno LazyColumn:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TarefaScreen(viewModel: TarefaViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Scaffold(
topBar = {
TopAppBar(title = { Text("Tarefas Nativo (Compose)") })
},
floatingActionButton = {
FloatingActionButton(onClick = { viewModel.abrirDialogNovo() }) {
Icon(Icons.Default.Add, contentDescription = "Nova Tarefa")
}
}
) { paddingValues ->
Column(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
FiltroSegmentedControl(
filtroSelecionado = uiState.filtro,
onSelectFiltro = { viewModel.definirFiltro(it) },
totalTodas = uiState.totalTarefas,
totalPendentes = uiState.totalPendentes,
totalConcluidas = uiState.totalConcluidas
)
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(
items = uiState.tarefasFiltradas,
key = { it.id ?: 0L }
) { tarefa ->
TarefaCard(
tarefa = tarefa,
onToggle = { viewModel.alternarStatus(tarefa) },
onEdit = { viewModel.abrirEdicao(tarefa) },
onDelete = { tarefa.id?.let { viewModel.excluirTarefa(it) } }
)
}
}
}
}
}
Para validar o funcionamento integrado do aplicativo Android com a API Spring Boot:
Abra um terminal na pasta da API e inicie o serviço:
cd catalogo_projetos/fullstack_tarefas_01_spring_api/api-spring
./mvnw spring-boot:run
Verifique se a API está ativa:
curl -i http://localhost:8080/api/tarefas
mobile-android-compose/ no Android Studio.| Checkpoint | Ação Realizada | Resultado Esperado | Status |
|---|---|---|---|
| CP-01 | Inicializar o app no Emulador | Conexão imediata com 10.0.2.2:8080; tarefas cadastradas na Parte 01 são renderizadas. |
PASS ✅ |
| CP-02 | Tocar no botão flutuante + (FAB) |
Modal TarefaDialog surge na tela com campo de texto e foco automático. |
PASS ✅ |
| CP-03 | Digitar “Dominar Jetpack Compose” e salvar | Requisição POST disparada; novo card surge no topo da lista sem recarregar a tela. |
PASS ✅ |
| CP-04 | Tocar no checkbox de uma tarefa | Animação de fade acionada, texto recebe tachado e PUT /api/tarefas/{id} é confirmado. |
PASS ✅ |
| CP-05 | Tocar nos chips de filtro (“Pendentes” / “Concluídas”) | A listagem filtra instantaneamente na memória e os contadores atualizam dinamicamente. | PASS ✅ |
| CP-06 | Tocar no ícone de lixeira de um card | Requisição DELETE /api/tarefas/{id} executada; card é removido do LazyColumn. |
PASS ✅ |
Com a conclusão da Parte 10, o ecossistema Lista de Tarefas atinge a plenitude de sua proposta pedagógica:
RecyclerView (Parte 04).Você agora possui um panorama completo, comparativo e prático de toda a engenharia de software contemporânea aplicada a sistemas corporativos distribuídos!