Model-View-ViewModel en Jetpack Compose
Aprende paso a paso cómo implementar MVVM en una calculadora:
MVVM (Model-View-ViewModel) es un patrón de arquitectura que separa la lógica de negocio de la interfaz de usuario, facilitando el mantenimiento y las pruebas.
Composables - UI que el usuario ve
Lógica de presentación - Gestiona estados
Data Classes - Estructura de datos
Crearemos una aplicación para sumar dos números siguiendo la arquitectura MVVM.
El Modelo representa la estructura de datos de nuestra calculadora:
data class Calculadora(
val num1: String = "",
val num2: String = "",
val resultado: Int = 0,
val isErrorNum1: Boolean = false,
val isErrorNum2: Boolean = false,
val isEnabled: Boolean = false
)
Al usar data class obtenemos automáticamente métodos como
copy() que nos permite crear copias inmutables con valores modificados.
El ViewModel gestiona el estado y contiene la lógica de negocio:
class CalculadoraViewModel : ViewModel() {
// Estado mutable privado (solo escritura interna)
private val _calculadora = MutableStateFlow(Calculadora())
// Estado público de solo lectura
val calculadora: StateFlow<Calculadora> get() = _calculadora
}
fun getNum1(num1: String) {
_calculadora.update {
it.copy(
num1 = num1,
isErrorNum1 = num1.isBlank(),
isEnabled = !num1.isBlank() && !it.num2.isBlank()
)
}
}
_calculadora.update → Actualiza el estado actualit.copy() → Crea una copia inmutable con nuevos valoresnum1 = num1 → Guarda el valor ingresadoisErrorNum1 = num1.isBlank() → true si está vacío (error)isEnabled → Habilita el botón solo si ambos campos tienen datosfun getNum2(num2: String) {
_calculadora.update {
it.copy(
num2 = num2,
isErrorNum2 = num2.isBlank(),
isEnabled = !it.num1.isBlank() && !num2.isBlank()
)
}
}
fun sumar() {
val num1 = _calculadora.value.num1
val num2 = _calculadora.value.num2
// Validar que no estén vacíos
if (num1.isBlank() || num2.isBlank()) return
// Intentar convertir a enteros
val numero1 = num1.toIntOrNull()
val numero2 = num2.toIntOrNull()
if (numero1 != null && numero2 != null) {
// Suma exitosa
_calculadora.update {
it.copy(resultado = numero1 + numero2)
}
} else {
// Error: no son números válidos
_calculadora.update {
it.copy(
isErrorNum1 = numero1 == null,
isErrorNum2 = numero2 == null
)
}
}
}
Usamos toIntOrNull() para convertir el String a Int. Si la
conversión falla (el usuario ingresó letras), devuelve null y mostramos el error
correspondiente.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
// Crear o recuperar instancia del ViewModel
val viewModel: CalculadoraViewModel = viewModel()
ShowContent(viewModel)
}
}
}
Esta función crea una nueva instancia del ViewModel o recupera una existente. El ViewModel sobrevive a cambios de configuración como rotaciones de pantalla.
@Composable
fun ShowContent(viewModel: CalculadoraViewModel) {
Scaffold {
App(it, viewModel)
}
}
@Composable
fun App(paddingValues: PaddingValues, viewModel: CalculadoraViewModel) {
// Convertir StateFlow a State observable
val calculadora by viewModel.calculadora.collectAsState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally
) {
TextUi("Suma de dos números")
// Primer TextField
TextFieldUi(
value = calculadora.num1,
text = "Ingrese un número entero",
placeholder = "Ej: 5",
isError = calculadora.isErrorNum1
) {
viewModel.getNum1(it)
}
// Segundo TextField
TextFieldUi(
value = calculadora.num2,
text = "Ingrese un segundo número entero",
placeholder = "Ej: 3",
isError = calculadora.isErrorNum2
) {
viewModel.getNum2(it)
}
// Botón habilitado solo si ambos campos tienen datos
ButtonUi("Calcular", calculadora.isEnabled) {
viewModel.sumar()
}
// Mostrar resultado
TextUi("Resultado: ${calculadora.resultado}")
}
}
Convierte un StateFlow en un State observable de Compose.
Cuando el StateFlow emite un nuevo valor, el composable se recompone automáticamente.
El TextField captura el texto y ejecuta su lambda
onValueChange
El valor ingresado se pasa al ViewModel
Se usa _calculadora.update para crear un nuevo
estado con los valores actualizados
Los observadores son notificados del cambio
Compose detecta el cambio y actualiza la interfaz automáticamente
La lógica está separada de la UI, facilitando las pruebas unitarias
El ViewModel puede usarse en múltiples vistas
El estado se mantiene al rotar el dispositivo
Separación clara de responsabilidades
Más fácil de modificar y escalar
Múltiples desarrolladores pueden trabajar en paralelo