Creación de un ViewModel para Gestión de Tareas en Pantalla

Resumen

Conectar la interfaz de creación de tareas con su lógica requiere un ViewModel que gestione estados, acciones y eventos en Jetpack Compose. Aquí aprendes a estructurarlo usando TextFieldState, sealed interfaces y un canal de eventos para feedback al usuario.

Cómo separar el estado de la pantalla en una clase independiente

El primer paso es mover TaskScreenState a su propio archivo dentro del paquete details. Esto mantiene el código modular y facilita el mantenimiento cuando la pantalla crece.

Dentro de ese estado, en lugar de rastrear los campos de texto con un String, conviene usar TextFieldState, una clase especializada de Compose que se encarga del seguimiento por ti. Así evitas declarar variables de estado independientes para cada TextField y reduces el código repetitivo.

¿Qué es TextFieldState en Jetpack Compose? Es un state holder especializado que gestiona el contenido de un campo de texto sin que tengas que mantener un String mutable por separado. Simplifica el manejo de formularios.

Cómo definir acciones y eventos con sealed interfaces

Siguiendo el mismo patrón de la lista de tareas, modelas las interacciones del usuario como una sealed interface llamada ActionTask. Cada acción representa algo que puede ocurrir en la UI [02:10].

Las acciones necesarias son:

  • SaveTask: como data object, dispara el guardado de la tarea.
  • Back: como data object, indica retorno a la lista.
  • ChangeTaskCategory: como data class que recibe una Category nullable.
  • ChangeTaskDone: como data class para alternar el estado de completado.

Los eventos viven en otra sealed interface y, en este caso, basta con uno: TaskSaved. Este evento notifica a la UI que la tarea se persistió correctamente para mostrar un toast.

Cómo construir el TaskViewModel paso a paso

La clase TaskViewModel hereda de ViewModel e instancia el FakeTaskLocalDataSource para persistir datos en memoria [04:30]. Dentro declaras tres piezas clave.

Cómo exponer el estado con mutableStateOf

El estado se expone como una variable mutableStateOf<TaskScreenState> con inicialización by lazy y private set. El private set impide que la vista modifique el estado directamente, manteniendo la unidirección del flujo de datos.

Cómo crear un canal de eventos en Kotlin

Los eventos se emiten a través de un Channel<TaskEvent> privado, expuesto como Flow mediante receiveAsFlow(). Así la UI los consume una sola vez sin reproducirlos en recomposiciones.

¿Para qué sirve un Channel en un ViewModel? Permite enviar eventos puntuales (como mostrar un toast o navegar) que no deben quedar guardados en el estado ni dispararse dos veces.

Cómo manejar acciones con when y viewModelScope

La función onAction(action: ActionTask) usa una sentencia when para discriminar cada acción:

  • ChangeTaskCategory actualiza el estado con state.copy(category = ...).
  • ChangeTaskDone alterna el flag de completado.
  • SaveTask construye un objeto Task y lo persiste.
  • Back se delega a la navegación, igual que en la lista.

Para guardar, generas un ID único con UUID.randomUUID().toString(), tomas el título y descripción desde los TextFieldState accediendo a su charSequence y convirtiendo a String, y llamas a fakeTaskLocalDataSource.addTask(task). Como addTask es una suspend function, debes envolverla en viewModelScope.launch [07:45]. Tras guardar, emites eventChannel.send(TaskEvent.TaskSaved).

Cómo conectar el ViewModel con la pantalla composable

La pantalla se divide en dos: TaskScreenRoot (con acceso al ViewModel) y TaskScreen (puramente presentacional). Esta separación facilita los previews y las pruebas.

En TaskScreenRoot instancias el ViewModel con viewModel(), extraes el estado y observas los eventos con events.collect. Cuando llega TaskSaved, obtienes el contexto con LocalContext.current y muestras un Toast.makeText con un string resource llamado task_saved.

La lambda onAction: (ActionTask) -> Unit se conecta directamente con viewModel::onAction, manteniendo a TaskScreen desacoplada del ViewModel.

Cómo ajustar los TextFields al nuevo estado

Al cambiar de String a TextFieldState, los campos requieren ajustes:

  • Para validar vacío usa state.taskName.text.toString().isEmpty().
  • Reemplaza value por state directamente.
  • Cambia decorationBox por decorator.
  • Elimina onValueChange porque el estado se actualiza solo.
  • Sustituye maxLines por lineLimits = TextFieldLineLimits.SingleLine en el campo de nombre.

Los previews también se actualizan: en lugar de pasar Strings, inicializas TextFieldState("task 1") para cada caso.

Cómo cablear las acciones a los componentes de la UI

En el Scaffold, el navigationIcon de la top bar usa Icons.AutoMirrored.Filled.ArrowBack con un tint basado en colorScheme.onSurface y un Modifier.clickable { onAction(ActionTask.Back) }.

El Checkbox de completado dispara onAction(ActionTask.ChangeTaskDone). El menú desplegable de categorías cierra con isExpanded = false y emite onAction(ActionTask.ChangeTaskCategory(category)) al seleccionar. El botón de guardar dispara ActionTask.SaveTask.

Al probar desde MainActivity invocando TaskScreenRoot, ya puedes escribir el nombre, ver cómo crece la descripción, marcar como completada, elegir entre work, personal, shopping u other, y recibir el toast de confirmación al guardar.

Con esto tienes dos pantallas funcionales: la lista y la creación. ¿Cómo conectarías ahora la navegación entre ambas? Comparte un screenshot de tu proyecto y nos vemos en la siguiente clase para enlazarlas.