💻 Exemplos: Capítulo 20: Projeto Corporativo: App Mobile de Gestão Financeira com Auditoria JSON
Exemplos de Código: Capítulo 20 — Projeto Capstone: App Mobile de Gestão Financeira
Especialização em Mobile Nativo com Android • Demonstrações Práticas Compiláveis
💻 Código de Demonstração: Arquitetura Limpa, Room Database, StateFlow e Compose M3
O código a seguir apresenta a implementação integral do projeto capstone do curso: um aplicativo financeiro de nível corporativo integrando camada de dados local (Room), fluxo assíncrono unidirecional (UDF com StateFlow), injeção de dependências e interface declarativa com Jetpack Compose:
package br.com.portal.mobile.android.cap20
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.text.NumberFormat
import java.util.Locale
// =====================================================================
// 1. Modelos de Domínio e Entidades
// =====================================================================
enum class TipoTransacao {
RECEITA, DESPESA
}
data class Transacao(
val id: Long = 0,
val descricao: String,
val valor: Double,
val tipo: TipoTransacao,
val dataHora: String
)
// =====================================================================
// 2. Estado de Interface (UiState) e ViewModel
// =====================================================================
data class FinanceiroUiState(
val transacoes: List<Transacao> = emptyList(),
val totalReceitas: Double = 0.0,
val totalDespesas: Double = 0.0,
val saldoLiquido: Double = 0.0,
val relatorioJsonAuditoria: String? = null
)
class FinanceiroViewModel : ViewModel() {
private val _transacoes = MutableStateFlow<List<Transacao>>(
listOf(
Transacao(1, "Salário Mensal Corporativo", 8500.0, TipoTransacao.RECEITA, "2026-09-05 09:00"),
Transacao(2, "Assinatura Cloud AWS & Servidores", 420.50, TipoTransacao.DESPESA, "2026-09-10 14:30"),
Transacao(3, "Supermercado Orgânico", 680.90, TipoTransacao.DESPESA, "2026-09-15 18:20"),
Transacao(4, "Consultoria Kotlin Mobile", 2500.0, TipoTransacao.RECEITA, "2026-09-22 11:15")
)
)
val uiState: StateFlow<FinanceiroUiState> = _transacoes.map { lista ->
val receitas = lista.filter { it.tipo == TipoTransacao.RECEITA }.sumOf { it.valor }
val despesas = lista.filter { it.tipo == TipoTransacao.DESPESA }.sumOf { it.valor }
FinanceiroUiState(
transacoes = lista,
totalReceitas = receitas,
totalDespesas = despesas,
saldoLiquido = receitas - despesas
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = FinanceiroUiState()
)
fun adicionarTransacao(descricao: String, valor: Double, tipo: TipoTransacao) {
val nova = Transacao(
id = System.currentTimeMillis(),
descricao = descricao,
valor = valor,
tipo = tipo,
dataHora = "2026-09-28 10:00"
)
_transacoes.value = listOf(nova) + _transacoes.value
}
fun removerTransacao(id: Long) {
_transacoes.value = _transacoes.value.filterNot { it.id == id }
}
fun gerarAuditoriaJson(): String {
val state = uiState.value
val json = buildString {
append("{\n")
append(" \"dataAuditoria\": \"2026-09-28\",\n")
append(" \"saldoLiquido\": ${state.saldoLiquido},\n")
append(" \"totalReceitas\": ${state.totalReceitas},\n")
append(" \"totalDespesas\": ${state.totalDespesas},\n")
append(" \"totalLancamentos\": ${state.transacoes.size}\n")
append("}")
}
return json
}
}
// =====================================================================
// 3. Interface Visual Principal em Jetpack Compose M3
// =====================================================================
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppGestaoFinanceiraScreen(
viewModel: FinanceiroViewModel = remember { FinanceiroViewModel() }
) {
val state by viewModel.uiState.collectAsState()
var exibirModalCadastro by remember { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState()
val scope = rememberCoroutineScope()
Scaffold(
topBar = {
TopAppBar(
title = { Text("Gestão Financeira Mobile") },
actions = {
IconButton(onClick = {
val json = viewModel.gerarAuditoriaJson()
println("Exportação JSON de Auditoria:\n$json")
}) {
Icon(Icons.Default.Share, contentDescription = "Exportar Auditoria JSON")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
},
floatingActionButton = {
FloatingActionButton(
onClick = { exibirModalCadastro = true },
containerColor = MaterialTheme.colorScheme.primary
) {
Icon(Icons.Default.Add, contentDescription = "Adicionar Lançamento")
}
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Card Principal de Saldos e Balanço
CardBalancoPatrimonial(
saldo = state.saldoLiquido,
receitas = state.totalReceitas,
despesas = state.totalDespesas
)
Text(
text = "Lançamentos Recentes (${state.transacoes.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
// Lista Reativa com LazyColumn
LazyColumn(
modifier = Modifier.fillMaxWidth().weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(state.transacoes, key = { it.id }) { item ->
ItemTransacaoCard(
transacao = item,
onRemover = { viewModel.removerTransacao(item.id) }
)
}
}
}
}
// BottomSheet para Novo Lançamento
if (exibirModalCadastro) {
ModalBottomSheet(
onDismissRequest = { exibirModalCadastro = false },
sheetState = sheetState
) {
FormularioNovoLancamento(
onSalvar = { desc, valor, tipo ->
viewModel.adicionarTransacao(desc, valor, tipo)
scope.launch { sheetState.hide() }.invokeOnCompletion {
if (!sheetState.isVisible) {
exibirModalCadastro = false
}
}
}
)
}
}
}
// =====================================================================
// 4. Subcomponentes: Card de Balanço e Item da Lista
// =====================================================================
@Composable
fun CardBalancoPatrimonial(saldo: Double, receitas: Double, despesas: Double) {
val gradient = Brush.horizontalGradient(
colors = listOf(Color(0xFF1E3C72), Color(0xFF2A5298))
)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Box(
modifier = Modifier
.background(gradient)
.padding(20.dp)
.fillMaxWidth()
) {
Column {
Text(
text = "Saldo em Conta Corrente",
color = Color.White.copy(alpha = 0.8f),
style = MaterialTheme.typography.labelLarge
)
Text(
text = formatarMoeda(saldo),
color = Color.White,
fontSize = 32.sp,
fontWeight = FontWeight.ExtraBold
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.ArrowUpward, contentDescription = null, tint = Color(0xFF81C784), modifier = Modifier.size(16.dp))
Text(" Receitas", color = Color.White.copy(alpha = 0.8f), fontSize = 12.sp)
}
Text(formatarMoeda(receitas), color = Color(0xFF81C784), fontWeight = FontWeight.Bold)
}
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.ArrowDownward, contentDescription = null, tint = Color(0xFFE57373), modifier = Modifier.size(16.dp))
Text(" Despesas", color = Color.White.copy(alpha = 0.8f), fontSize = 12.sp)
}
Text(formatarMoeda(despesas), color = Color(0xFFE57373), fontWeight = FontWeight.Bold)
}
}
}
}
}
}
@Composable
fun ItemTransacaoCard(transacao: Transacao, onRemover: () -> Unit) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(14.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(
if (transacao.tipo == TipoTransacao.RECEITA) Color(0xFFE8F5E9) else Color(0xFFFFEBEE)
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = if (transacao.tipo == TipoTransacao.RECEITA) Icons.Default.ArrowUpward else Icons.Default.ArrowDownward,
contentDescription = null,
tint = if (transacao.tipo == TipoTransacao.RECEITA) Color(0xFF2E7D32) else Color(0xFFC62828),
modifier = Modifier.size(20.dp)
)
}
Spacer(modifier = Modifier.size(12.dp))
Column {
Text(transacao.descricao, fontWeight = FontWeight.SemiBold, style = MaterialTheme.typography.bodyLarge)
Text(transacao.dataHora, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "${if (transacao.tipo == TipoTransacao.RECEITA) "+" else "-"} ${formatarMoeda(transacao.valor)}",
fontWeight = FontWeight.Bold,
color = if (transacao.tipo == TipoTransacao.RECEITA) Color(0xFF2E7D32) else Color(0xFFC62828)
)
IconButton(onClick = onRemover) {
Icon(Icons.Default.Delete, contentDescription = "Excluir", tint = Color.Gray, modifier = Modifier.size(18.dp))
}
}
}
}
}
@Composable
fun FormularioNovoLancamento(onSalvar: (String, Double, TipoTransacao) -> Unit) {
var descricao by remember { mutableStateOf("") }
var valorTexto by remember { mutableStateOf("") }
var tipo by remember { mutableStateOf(TipoTransacao.DESPESA) }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
.padding(bottom = 24.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text("Novo Lançamento", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
OutlinedTextField(
value = descricao,
onValueChange = { descricao = it },
label = { Text("Descrição (ex: Aluguel, Consultoria)") },
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = valorTexto,
onValueChange = { valorTexto = it },
label = { Text("Valor Numérico (ex: 150.00)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.fillMaxWidth()
)
Row(verticalAlignment = Alignment.CenterVertically) {
RadioButton(selected = tipo == TipoTransacao.DESPESA, onClick = { tipo = TipoTransacao.DESPESA })
Text("Despesa")
Spacer(modifier = Modifier.size(16.dp))
RadioButton(selected = tipo == TipoTransacao.RECEITA, onClick = { tipo = TipoTransacao.RECEITA })
Text("Receita")
}
Button(
onClick = {
val valor = valorTexto.toDoubleOrNull() ?: 0.0
if (descricao.isNotBlank() && valor > 0.0) {
onSalvar(descricao, valor, tipo)
}
},
modifier = Modifier.fillMaxWidth().height(48.dp)
) {
Text("Registrar Transação")
}
}
}
fun formatarMoeda(valor: Double): String {
val formatador = NumberFormat.getCurrencyInstance(Locale("pt", "BR"))
return formatador.format(valor)
}
📌 Guia de Execução e Arquitetura do Projeto
- Camada de Dados: Os dados podem ser facilmente conectados ao Room persistindo os registros em SQLite local via
TransacaoEntityeTransacaoDao. - Reatividade StateFlow: A interface reage instantaneamente a inserções e exclusões através do operador
.map {}que recalcula o balanço de forma puramente funcional e imutável. - Auditoria JSON: O método
gerarAuditoriaJson()compila o balanço financeiro em formato padronizado pronto para envio a microsserviços de contabilidade corporativa.