🧮 Calculadora MVVM - Parte 2

4 Operaciones + Manejo de Errores con Result

🎥 Video Tutorial

Continúa con la segunda parte de la calculadora en MVVM:

🎯 Objetivos de esta Parte

En esta segunda parte extenderemos la calculadora para incluir las 4 operaciones básicas usando conceptos avanzados:

🔢 4 Operaciones

  • Suma (+)
  • Resta (-)
  • Multiplicación (*)
  • División (/)

🎨 Enum Class

  • Valores constantes
  • Type-safe
  • Fácil de mantener

✅ Result Type

  • Manejo de errores
  • División por cero
  • Código más seguro

1️⃣ Paso 1: Actualizar RadioButton

Modificaremos el componente RadioButton para trabajar con Enum Class. Si necesitas repasar RadioButtons, revisa aquí.

@Composable
fun RadioButtonUi(
    lista: List<Operacion>,
    value: Operacion,
    onClick: (Operacion) -> Unit
) {
    Row(
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        modifier = Modifier.fillMaxWidth()
    ) {
        lista.forEach { item ->
            Row(
                verticalAlignment = Alignment.CenterVertically,
                modifier = Modifier.padding(4.dp)
            ) {
                RadioButton(
                    selected = value == item,
                    onClick = { onClick(item) }
                )
                Text(
                    text = item.signo,
                    fontSize = 18.sp
                )
            }
        }
    }
}
🔍 Cambios importantes:
  • Ahora recibe List<Operacion> en lugar de List<String>
  • El parámetro value es de tipo Operacion (enum)
  • Accedemos al signo con item.signo
  • Usamos un Row principal para organizar horizontalmente

2️⃣ Paso 2: Crear Enum Class

Una Enum Class define un conjunto limitado de valores constantes. Es perfecta para operaciones matemáticas.

enum class Operacion(val signo: String) {
    SUMAR("+"),
    RESTAR("-"),
    MULTIPLICAR("*"),
    DIVIDIR("/")
}
🔑 Conceptos del Enum:
  • Constructor: (val signo: String) - Cada constante puede tener propiedades
  • Instancias: Cada constante (SUMAR, RESTAR, etc.) es una instancia de Operacion
  • Invocación: Al escribir SUMAR("+"), se invoca el constructor pasando "+" como argumento
  • Acceso: Podemos acceder al signo con operacion.signo

Valores del Enum:

SUMAR (+) RESTAR (-) MULTIPLICAR (*) DIVIDIR (/)
✅ Ventajas de usar Enum:
  • Type-safe: El compilador valida que solo uses valores válidos
  • Autocompletado: El IDE sugiere todas las opciones disponibles
  • Mantenible: Agregar operaciones es fácil y seguro
  • Sin errores de typo: No puedes escribir "suma" en lugar de "SUMAR"

3️⃣ Paso 3: Interfaz con Result

Crearemos una interfaz que usa Result<T> para manejar éxitos y errores de forma elegante.

interface Operaciones {
    fun calcular(
        operacion: Operacion,
        num1: Int,
        num2: Int
    ): Result<Int>
}
💡 ¿Qué es Result<T>?

Result es una clase genérica de Kotlin que representa el resultado de una operación que puede ser:

  • Éxito: Result.success(valor) - La operación se completó correctamente
  • Fallo: Result.failure(excepción) - Ocurrió un error

Flujo con Result:

Operación
¿Éxito?
✓ SI
Result.success(valor)
✗ NO
Result.failure(error)

4️⃣ Paso 4: Actualizar el Modelo

Modificamos la data class Calculadora para usar Result y Operacion:

data class Calculadora(
    val num1: String = "",
    val num2: String = "",
    val resultado: Result<Int> = Result.success(0),
    val operacion: Operacion = Operacion.SUMAR,
    val isErrorNum1: Boolean = false,
    val isErrorNum2: Boolean = false,
    val isEnabled: Boolean = false
)
⚠️ Cambios importantes:
  • resultado ahora es Result<Int> en lugar de Int
  • Agregamos operacion: Operacion con valor inicial SUMAR
  • El valor inicial de resultado es Result.success(0)

5️⃣ Paso 5: Implementar la Interfaz

Creamos la clase que implementa la lógica de las operaciones:

class CalculadoraImpl : Operaciones {

    override fun calcular(
        operacion: Operacion,
        num1: Int,
        num2: Int
    ): Result<Int> {
        return try {
            when (operacion) {
                Operacion.SUMAR -> 
                    Result.success(num1 + num2)
                
                Operacion.RESTAR -> 
                    Result.success(num1 - num2)
                
                Operacion.MULTIPLICAR -> 
                    Result.success(num1 * num2)
                
                Operacion.DIVIDIR -> {
                    if (num2 == 0) {
                        Result.failure(
                            ArithmeticException("No se puede dividir por cero")
                        )
                    } else {
                        Result.success(num1 / num2)
                    }
                }
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}
🔍 Explicación del When:

La expresión when evalúa el valor del enum operacion y ejecuta el bloque correspondiente. Es como un switch en otros lenguajes, pero más poderoso:

  • El compilador verifica que cubras todos los casos del enum
  • No necesitas break como en switch
  • Puede retornar valores directamente
✅ Manejo de errores:
  • División por cero: Retorna Result.failure con un mensaje descriptivo
  • Try-catch: Captura cualquier excepción inesperada
  • Type-safe: El resultado siempre es un Result<Int>

6️⃣ Paso 6: Actualizar ViewModel

Agregamos una función para actualizar la operación seleccionada:

class CalculadoraViewModel : ViewModel() {

    private val _calculadora = MutableStateFlow(Calculadora())
    val calculadora: StateFlow<Calculadora> get() = _calculadora
    
    private val operaciones: Operaciones = CalculadoraImpl()

    // Función para actualizar la operación seleccionada
    fun getOperacion(operacion: Operacion) {
        _calculadora.update {
            it.copy(
                operacion = operacion,
                isEnabled = !it.num1.isBlank() && !it.num2.isBlank()
            )
        }
    }

    // Función para calcular según la operación seleccionada
    fun calcular() {
        val num1 = _calculadora.value.num1
        val num2 = _calculadora.value.num2

        if (num1.isBlank() || num2.isBlank()) return

        val numero1 = num1.toIntOrNull()
        val numero2 = num2.toIntOrNull()

        if (numero1 != null && numero2 != null) {
            val resultado = operaciones.calcular(
                _calculadora.value.operacion,
                numero1,
                numero2
            )
            
            _calculadora.update {
                it.copy(resultado = resultado)
            }
        } else {
            _calculadora.update {
                it.copy(
                    isErrorNum1 = numero1 == null,
                    isErrorNum2 = numero2 == null
                )
            }
        }
    }
}
💡 Inyección de dependencia simple:

Creamos una instancia de CalculadoraImpl en el ViewModel. En proyectos más grandes, usarías Hilt o Koin para inyección de dependencias.

7️⃣ Paso 7: Mostrar Resultado con Result

Usamos los métodos onSuccess y onFailure para manejar ambos casos:

@Composable
fun App(paddingValues: PaddingValues, viewModel: CalculadoraViewModel) {
    val calculadora by viewModel.calculadora.collectAsState()
    
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(paddingValues)
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        TextUi("Calculadora Completa")

        // Seleccionar operación
        RadioButtonUi(
            lista = Operacion.values().toList(),
            value = calculadora.operacion
        ) {
            viewModel.getOperacion(it)
        }

        // Campos de entrada
        TextFieldUi(
            value = calculadora.num1,
            text = "Primer número",
            placeholder = "Ej: 10",
            isError = calculadora.isErrorNum1
        ) {
            viewModel.getNum1(it)
        }

        TextFieldUi(
            value = calculadora.num2,
            text = "Segundo número",
            placeholder = "Ej: 5",
            isError = calculadora.isErrorNum2
        ) {
            viewModel.getNum2(it)
        }

        // Botón calcular
        ButtonUi("Calcular", calculadora.isEnabled) {
            viewModel.calcular()
        }

        // Mostrar resultado con manejo de éxito y error
        calculadora.resultado.onSuccess { valor ->
            TextUi("Resultado: $valor")
        }.onFailure { error ->
            Text(
                text = "Error: ${error.message}",
                color = Color.Red,
                fontSize = 16.sp
            )
        }
    }
}
🔑 Métodos de Result:
  • onSuccess { valor -> ... } - Se ejecuta solo si la operación fue exitosa
  • onFailure { error -> ... } - Se ejecuta solo si hubo un error
  • Encadenamiento: Puedes encadenar ambos métodos con punto
  • Type-safe: valor es Int y error es Throwable
✅ Ventajas de este enfoque:
  • No necesitas verificar manualmente si hubo error
  • El código es más legible y expresivo
  • Los errores se manejan de forma centralizada
  • Puedes mostrar mensajes personalizados según el error

🔄 Flujo Completo de la Aplicación

1 Usuario selecciona operación

RadioButton captura la selección y llama a viewModel.getOperacion(operacion)

2 Usuario ingresa números

Los TextFields llaman a getNum1() y getNum2()

3 Usuario presiona Calcular

Se ejecuta viewModel.calcular()

4 ViewModel valida y calcula

Convierte strings a Int, valida y llama a operaciones.calcular()

5 CalculadoraImpl procesa

Ejecuta la operación según el enum y retorna Result

6 UI muestra resultado

Usa onSuccess para mostrar el valor o onFailure para el error

💡 Operacion.values()

Para obtener todas las constantes del enum usamos values():

// Obtener todas las operaciones
val todasLasOperaciones = Operacion.values().toList()

// Resultado: [SUMAR, RESTAR, MULTIPLICAR, DIVIDIR]

RadioButtonUi(
    lista = Operacion.values().toList(),
    value = calculadora.operacion
) {
    viewModel.getOperacion(it)
}
📚 Métodos útiles de Enum:
  • values() - Retorna un array con todas las constantes
  • valueOf(name) - Obtiene una constante por su nombre
  • name - Nombre de la constante como String
  • ordinal - Posición de la constante (0, 1, 2...)

Ventajas de esta Arquitectura

🧪 Testeable

Puedes probar CalculadoraImpl sin UI

🔒 Type-Safe

El enum previene errores de valores inválidos

🛡️ Seguro

Result maneja errores de forma explícita

📦 Separado

Lógica separada de la interfaz

🔧 Extensible

Fácil agregar más operaciones

🧹 Limpio

Código organizado y mantenible

🚨 Casos de Error Manejados

❌ Errores que manejamos:
  • División por cero: Retorna mensaje específico
  • Entrada no numérica: Valida con toIntOrNull()
  • Campos vacíos: Deshabilita el botón calcular
  • Excepciones inesperadas: Capturadas en el catch
✅ Experiencia de usuario:
  • Mensajes de error claros y descriptivos
  • Validación en tiempo real
  • No se producen crashes
  • Feedback visual inmediato

💪 Ejercicio Propuesto

Extiende la calculadora con operaciones avanzadas:

1 Agrega nuevas operaciones

Añade POTENCIA (^) y MODULO (%) al enum

2 Implementa la lógica

Usa Math.pow() para potencia y el operador % para módulo

3 Maneja casos especiales

Potencias negativas, overflow, módulo por cero

🎯 Bonus:

Agrega un historial de operaciones que guarde las últimas 10 operaciones realizadas en una lista. Muéstralas en un LazyColumn.

📋 Código Completo Resumido

1. Enum (Operacion.kt)

enum class Operacion(val signo: String) {
    SUMAR("+"),
    RESTAR("-"),
    MULTIPLICAR("*"),
    DIVIDIR("/")
}

2. Interface (Operaciones.kt)

interface Operaciones {
    fun calcular(operacion: Operacion, num1: Int, num2: Int): Result<Int>
}

3. Implementación (CalculadoraImpl.kt)

class CalculadoraImpl : Operaciones {
    override fun calcular(operacion: Operacion, num1: Int, num2: Int): Result<Int> {
        return try {
            when (operacion) {
                Operacion.SUMAR -> Result.success(num1 + num2)
                Operacion.RESTAR -> Result.success(num1 - num2)
                Operacion.MULTIPLICAR -> Result.success(num1 * num2)
                Operacion.DIVIDIR -> {
                    if (num2 == 0) {
                        Result.failure(ArithmeticException("No se puede dividir por cero"))
                    } else {
                        Result.success(num1 / num2)
                    }
                }
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

4. Modelo Actualizado (Calculadora.kt)

data class Calculadora(
    val num1: String = "",
    val num2: String = "",
    val resultado: Result<Int> = Result.success(0),
    val operacion: Operacion = Operacion.SUMAR,
    val isErrorNum1: Boolean = false,
    val isErrorNum2: Boolean = false,
    val isEnabled: Boolean = false
)

🎓 Conclusión

✅ Has aprendido:
  • Usar Enum Class para valores constantes type-safe
  • Implementar interfaces para abstraer lógica
  • Manejar errores con Result<T>
  • Usar when con enumeradores
  • Separar responsabilidades en capas claras
  • Crear una calculadora completa con MVVM
🚀 Próximos pasos:

Ahora que dominas MVVM con manejo de errores, puedes explorar Room Database para persistir datos, Retrofit para APIs REST, y Hilt para inyección de dependencias profesional.