Modelo de datos para una to do app

Resumen

Antes de pintar pantallas en Jetpack Compose, conviene pensar la lógica: qué es una tarea, qué datos guarda y qué operaciones necesitas. Aquí armas el modelo de dominio de una to do app siguiendo clean architecture, defines la abstracción de la fuente de datos y construyes una implementación falsa en memoria con Flow para reaccionar a los cambios.

Esta guía te sirve si estás aprendiendo Android moderno con Kotlin y quieres entender cómo separar dominio, datos y UI sin saltarte pasos.

Cómo modelar una tarea con data class y enum en Kotlin

Lo primero es identificar qué representa una tarea dentro de tu aplicación. Para eso creas un paquete domain que albergará el modelo, y dentro defines la data class Task [01:00].

Una tarea no necesita 20 campos, necesita los justos:

  • Un id de tipo String para identificarla de forma única.
  • Un title de tipo String, obligatorio.
  • Una description de tipo String?, nulable, porque a veces solo quieres anotar el título.
  • Un isCompleted: Boolean con valor por defecto false.
  • Una category: Category? opcional.

La categoría no es un string libre, es un enum class llamado Category con valores como WORK, PERSONAL, SHOPPING y OTHER [02:30]. Usar un enum te da seguridad de tipos y autocompletado, y evita errores tipográficos al asignar categorías.

¿Por qué usar una data class para Task? Porque Kotlin te genera automáticamente equals, hashCode, copy y toString, lo que te ahorra código repetitivo cuando comparas o actualizas tareas en una lista.

Qué es TaskLocalDataSource y cómo definir las operaciones CRUD

Con el modelo listo, necesitas declarar qué puedes hacer con esas tareas. Aquí entra el patrón de abstracción: una interface llamada TaskLocalDataSource que describe el contrato sin atarse a una implementación concreta [03:45].

Las operaciones que define son las clásicas del CRUD:

  • addTask(task: Task) para crear.
  • updateTask(task: Task) para actualizar.
  • removeTask(task: Task) para borrar una.
  • deleteAllTasks() para limpiar todo.
  • getTaskById(id: String): Task? para consultar una específica.
  • Una propiedad taskFlow: Flow<List<Task>> para observar la lista completa.

Todas las funciones se marcan como suspend, porque simulan operaciones asíncronas y eventualmente llamarán a delay, que solo puede invocarse desde otra función suspend o dentro de una coroutine.

Por qué exponer la lista como Flow y no como List

Un Flow te permite observar cambios en el tiempo. Si agregas, actualizas o borras una tarea, la UI recibe la nueva lista automáticamente sin que tengas que pedirla otra vez. Esto encaja perfecto con la recomposición de Compose.

Internamente vas a usar un MutableStateFlow<List<Task>> inicializado con emptyList(), y lo expones públicamente como Flow de solo lectura.

Cómo implementar FakeTaskLocalDataSource para simular una base de datos

La interfaz no hace nada por sí sola. Necesitas una implementación, y la primera versión va a ser falsa: vive solo en RAM. Por eso la clase se llama FakeTaskLocalDataSource [05:30] y se ubica en un paquete data.

Más adelante migrarás a Room, pero por ahora una lista mutable es suficiente para probar la lógica de la UI.

La implementación gira alrededor de un MutableStateFlow privado que guarda la lista actual. Cada operación sigue el mismo patrón:

  1. Toma el valor actual del flow y lo convierte a MutableList.
  2. Modifica la lista (agrega, actualiza o borra).
  3. Simula un retraso con delay para imitar I/O real.
  4. Reasigna el nuevo valor al flow para notificar a los observadores.

¿Cómo actualizo una tarea por id en una lista? Usas indexOfFirst { it.id == task.id }. Si devuelve un valor distinto de -1, reemplazas el elemento en esa posición. Si devuelve -1, la tarea no existe y no haces nada.

Para getTaskById el truco es usar taskFlow.value.firstOrNull { it.id == id }, que devuelve la tarea o null si no la encuentra. Por eso el tipo de retorno también es nulable.

Cómo probar el flow desde MainActivity con LaunchedEffect

Con la lógica armada, toca verificar que funciona. En MainActivity instancias el FakeTaskLocalDataSource y observas su flow desde un composable [09:15].

Los pasos clave dentro de tu pantalla son:

  • Declarar var text by remember { mutableStateOf("") } para guardar el contenido a mostrar.
  • Lanzar un LaunchedEffect(true) que recolecte taskFlow y actualice text con cada emisión.
  • Lanzar otro LaunchedEffect que llame a addTask con una tarea de prueba, usando UUID.randomUUID().toString() como id.
  • Pintar un Text(text) dentro del Scaffold.

Al correr la app verás el contenido de la lista renderizado en pantalla. Si el texto se solapa con la barra superior, aplicas el innerPadding que entrega el Scaffold al modifier del Text, y el margen queda respetado.

¿Qué hace LaunchedEffect en Compose? Ejecuta una coroutine atada al ciclo de vida del composable. Cuando la clave cambia, cancela la anterior y lanza una nueva. Con clave true corre una sola vez al entrar.

Con esta base ya tienes el dominio modelado, las operaciones definidas y una fuente de datos falsa que reacciona a cambios. ¿Tú qué categoría agregarías a tu propia to do app?