💻 Exemplos: Capítulo 10: Arquitetura MVVM/MVI com ViewModel e StateFlow
Exemplos de Código: Capítulo 10 — Arquitetura MVVM e MVI com StateFlow
Especialização em Mobile Nativo com Android • Demonstrações Práticas Compiláveis
💻 Código de Demonstração: ViewModel, StateFlow e Ciclo Unidirecional MVI
O código abaixo implementa a arquitetura recomendada pelo Google para Android moderno:
- Unidirectional Data Flow (UDF): Intenções do usuário (User Intents) são processadas pelo ViewModel, gerando um novo estado único e imutável.
- Exposição reativa de estado via
StateFlowprotegido por encapsulamento privadoMutableStateFlow. - Consumo seguro no Composable via
collectAsStateWithLifecycle(), que suspende a coleta quando a Activity entra em segundo plano para economizar bateria.
package br.com.portal.mobile.android.cap10
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.width
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
// =====================================================================
// 1. Estado da UI e Intenções do Usuário (MVI Contracts)
// =====================================================================
data class CarteiraUiState(
val saldo: Double = 0.0,
val rendimentoMensal: Double = 0.0,
val estaCarregando: Boolean = false,
val mensagemErro: String? = null
)
sealed interface CarteiraIntent {
data object AtualizarDados : CarteiraIntent
data class AplicarAporte(val valor: Double) : CarteiraIntent
data class RealizarResgate(val valor: Double) : CarteiraIntent
}
// =====================================================================
// 2. ViewModel Corporativo
// =====================================================================
class CarteiraViewModel : ViewModel() {
// Backing Property: Mutável apenas internamente
private val _uiState = MutableStateFlow(CarteiraUiState())
// Exposto à UI como StateFlow somente leitura
val uiState: StateFlow<CarteiraUiState> = _uiState.asStateFlow()
init {
processarIntent(CarteiraIntent.AtualizarDados)
}
fun processarIntent(intent: CarteiraIntent) {
when (intent) {
is CarteiraIntent.AtualizarDados -> carregarDadosServidor()
is CarteiraIntent.AplicarAporte -> aportar(intent.valor)
is CarteiraIntent.RealizarResgate -> resgatar(intent.valor)
}
}
private fun carregarDadosServidor() {
viewModelScope.launch {
_uiState.update { it.copy(estaCarregando = true, mensagemErro = null) }
delay(1200) // Simula chamada remota REST ou banco Room
_uiState.update {
it.copy(
saldo = 45850.75,
rendimentoMensal = 412.30,
estaCarregando = false
)
}
}
}
private fun aportar(valor: Double) {
_uiState.update { estadoAtual ->
estadoAtual.copy(saldo = estadoAtual.saldo + valor)
}
}
private fun resgatar(valor: Double) {
_uiState.update { estadoAtual ->
if (estadoAtual.saldo >= valor) {
estadoAtual.copy(saldo = estadoAtual.saldo - valor)
} else {
estadoAtual.copy(mensagemErro = "Saldo insuficiente para resgate.")
}
}
}
}
// =====================================================================
// 3. UI Reativa Desacoplada
// =====================================================================
@Composable
fun CarteiraScreen(viewModel: CarteiraViewModel = CarteiraViewModel()) {
// Coleta o StateFlow com ciclo de vida seguro (Lifecycle-Aware)
val state by viewModel.uiState.collectAsStateWithLifecycle()
CarteiraContent(
state = state,
onIntent = viewModel::processarIntent
)
}
@Composable
fun CarteiraContent(
state: CarteiraUiState,
onIntent: (CarteiraIntent) -> Unit,
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
if (state.estaCarregando) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Patrimônio Investido",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(12.dp))
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "R$ ${String.format("%.2f", state.saldo)}",
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Text(
text = "+ R$ ${String.format("%.2f", state.rendimentoMensal)} neste mês",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.secondary
)
}
}
state.mensagemErro?.let { erro ->
Spacer(modifier = Modifier.height(16.dp))
Text(text = erro, color = MaterialTheme.colorScheme.error)
}
Spacer(modifier = Modifier.height(32.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(onClick = { onIntent(CarteiraIntent.AplicarAporte(500.0)) }) {
Text("Aportar R$ 500")
}
OutlinedButton(onClick = { onIntent(CarteiraIntent.RealizarResgate(500.0)) }) {
Text("Resgatar R$ 500")
}
}
}
}
}
}
@Preview(showBackground = true)
@Composable
fun CarteiraPreview() {
MaterialTheme {
CarteiraContent(
state = CarteiraUiState(saldo = 50000.0, rendimentoMensal = 450.0),
onIntent = {}
)
}
}