🏗️ Arquitectura MVVM

Model-View-ViewModel en Jetpack Compose

🎥 Video Tutorial

Aprende paso a paso cómo implementar MVVM en una calculadora:

🎯 ¿Qué es MVVM?

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.

VIEW (Vista)

Composables - UI que el usuario ve

↕️
VIEWMODEL

Lógica de presentación - Gestiona estados

↕️
MODEL (Modelo)

Data Classes - Estructura de datos

📱 View (Vista)

  • Composables
  • Muestra datos
  • Captura eventos
  • No tiene lógica

🧠 ViewModel

  • Maneja estados
  • Lógica de negocio
  • Expone datos
  • Sobrevive a rotaciones

📦 Model (Modelo)

  • Data Classes
  • Estructura de datos
  • Representa entidades
  • Sin lógica de UI

🔢 Proyecto: Calculadora con MVVM

Crearemos una aplicación para sumar dos números siguiendo la arquitectura MVVM.

📋 Componentes necesarios:
  • Scaffold para organizar la pantalla
  • Column para alinear elementos verticalmente
  • TextField para entrada de datos
  • Button para ejecutar la suma
  • PaddingValues para evitar superposiciones

1️⃣ Paso 1: Crear el Modelo de Datos

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
)
🔍 Propiedades del Modelo:
  • num1, num2: Valores ingresados por el usuario
  • resultado: Resultado de la suma
  • isErrorNum1, isErrorNum2: Indican si hay errores de validación
  • isEnabled: Habilita/deshabilita el botón de sumar
✅ Ventajas del Data Class:

Al usar data class obtenemos automáticamente métodos como copy() que nos permite crear copias inmutables con valores modificados.

2️⃣ Paso 2: Crear el ViewModel

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
}
💡 MutableStateFlow vs StateFlow:
  • MutableStateFlow: Contenedor de estado observable que permite modificar valores. Es privado para escritura interna.
  • Versión de solo lectura que se expone públicamente. Los observadores reciben notificaciones cuando cambia el estado.

Función para obtener el primer número

fun getNum1(num1: String) {
    _calculadora.update {
        it.copy(
            num1 = num1,
            isErrorNum1 = num1.isBlank(),
            isEnabled = !num1.isBlank() && !it.num2.isBlank()
        )
    }
}
🔍 Explicación línea por línea:
  • _calculadora.update → Actualiza el estado actual
  • it.copy() → Crea una copia inmutable con nuevos valores
  • num1 = num1 → Guarda el valor ingresado
  • isErrorNum1 = num1.isBlank() → true si está vacío (error)
  • isEnabled → Habilita el botón solo si ambos campos tienen datos

Función para obtener el segundo número

fun getNum2(num2: String) {
    _calculadora.update {
        it.copy(
            num2 = num2,
            isErrorNum2 = num2.isBlank(),
            isEnabled = !it.num1.isBlank() && !num2.isBlank()
        )
    }
}

Función para sumar

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
            )
        }
    }
}
⚠️ Validación importante:

Usamos toIntOrNull() para convertir el String a Int. Si la conversión falla (el usuario ingresó letras), devuelve null y mostramos el error correspondiente.

3️⃣ Paso 3: Crear la Vista (UI)

MainActivity - Crear instancia del ViewModel

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)
        }
    }
}
💡 Función 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.

Composables de la Vista

@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}")
    }
}
🔑 collectAsState():

Convierte un StateFlow en un State observable de Compose. Cuando el StateFlow emite un nuevo valor, el composable se recompone automáticamente.

🔄 Flujo Completo de la Aplicación

1 Usuario escribe en TextField

El TextField captura el texto y ejecuta su lambda onValueChange

2 Se invoca viewModel.getNum1(it)

El valor ingresado se pasa al ViewModel

3 ViewModel actualiza el estado

Se usa _calculadora.update para crear un nuevo estado con los valores actualizados

4 StateFlow emite el nuevo valor

Los observadores son notificados del cambio

5 UI se recompone

Compose detecta el cambio y actualiza la interfaz automáticamente

Ventajas de usar MVVM

🧪 Testeable

La lógica está separada de la UI, facilitando las pruebas unitarias

🔄 Reutilizable

El ViewModel puede usarse en múltiples vistas

📱 Sobrevive rotaciones

El estado se mantiene al rotar el dispositivo

🧹 Código limpio

Separación clara de responsabilidades

🔧 Mantenible

Más fácil de modificar y escalar

👥 Colaborativo

Múltiples desarrolladores pueden trabajar en paralelo