4 Operaciones + Manejo de Errores con Result
Continúa con la segunda parte de la calculadora en MVVM:
En esta segunda parte extenderemos la calculadora para incluir las 4 operaciones básicas usando conceptos avanzados:
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
)
}
}
}
}
List<Operacion> en lugar de List<String>value es de tipo Operacion (enum)item.signoUna 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("/")
}
(val signo: String) - Cada constante puede tener propiedadesSUMAR, RESTAR, etc.) es una instancia de OperacionSUMAR("+"), se invoca el constructor pasando "+" como argumentooperacion.signoCrearemos 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>
}
Result es una clase genérica de Kotlin que representa el resultado de una operación que puede ser:
Result.success(valor) - La operación se completó correctamenteResult.failure(excepción) - Ocurrió un errorModificamos 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
)
resultado ahora es Result<Int> en lugar de Intoperacion: Operacion con valor inicial SUMARResult.success(0)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)
}
}
}
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:
break como en switchResult.failure con un mensaje descriptivoResult<Int>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
)
}
}
}
}
Creamos una instancia de CalculadoraImpl en el ViewModel.
En proyectos más grandes, usarías Hilt o Koin para inyección de dependencias.
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
)
}
}
}
onSuccess { valor -> ... } - Se ejecuta solo si la operación fue exitosaonFailure { error -> ... } - Se ejecuta solo si hubo un errorvalor es Int y error es ThrowableRadioButton captura la selección y llama a viewModel.getOperacion(operacion)
Los TextFields llaman a getNum1() y getNum2()
Se ejecuta viewModel.calcular()
Convierte strings a Int, valida y llama a operaciones.calcular()
Ejecuta la operación según el enum y retorna Result
Usa onSuccess para mostrar el valor o onFailure para el error
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)
}
values() - Retorna un array con todas las constantesvalueOf(name) - Obtiene una constante por su nombrename - Nombre de la constante como Stringordinal - Posición de la constante (0, 1, 2...)Puedes probar CalculadoraImpl sin UI
El enum previene errores de valores inválidos
Result maneja errores de forma explícita
Lógica separada de la interfaz
Fácil agregar más operaciones
Código organizado y mantenible
toIntOrNull()Extiende la calculadora con operaciones avanzadas:
Añade POTENCIA (^) y MODULO (%) al enum
Usa Math.pow() para potencia y el operador % para módulo
Potencias negativas, overflow, módulo por cero
Agrega un historial de operaciones que guarde las últimas 10 operaciones realizadas en una lista. Muéstralas en un LazyColumn.
enum class Operacion(val signo: String) {
SUMAR("+"),
RESTAR("-"),
MULTIPLICAR("*"),
DIVIDIR("/")
}
interface Operaciones {
fun calcular(operacion: Operacion, num1: Int, num2: Int): Result<Int>
}
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)
}
}
}
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
)
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.