Desarrollar una app Android en ratos sueltos, entre semana y fin de semana, tiene un problema que no aparece en los tutoriales: el coste de retomar el contexto. Si dejas un proyecto una semana sin tocarlo y al volver tardas media hora en recordar dónde estaba cada cosa, esa media hora se repite cada vez que vuelves. En un proyecto que llevo desarrollando como Caladero, la app de pesca en la que trabajo en ratos libres, esto se nota mucho, así que he ido afinando una estructura que reduce ese coste al mínimo.
El problema que intento resolver
No es un problema de rendimiento ni de arquitectura "correcta" en abstracto. Es un problema de memoria personal: cuando programas a tiempo parcial, tu cerebro no mantiene caliente el estado del proyecto entre sesiones. La solución no es memorizar mejor, es estructurar el proyecto de forma que la propia carpeta te diga dónde está cada cosa sin tener que pensarlo.
Estructura de paquetes por función, no por tipo
El error clásico, y el que yo mismo cometí en mi primer proyecto serio, es organizar el código por tipo técnico: una carpeta activities, otra fragments, otra viewmodels, otra models. Con pocos archivos funciona, pero en cuanto el proyecto crece, para entender una sola funcionalidad tienes que saltar entre cinco carpetas distintas.
Ahora organizo por función o "feature". Por ejemplo, en Caladero tengo algo parecido a esto:
app/src/main/java/dev/caprini/caladero/
feature/puntos/
PuntosViewModel.kt
PuntosRepository.kt
ui/PuntosScreen.kt
data/PuntoEntity.kt
feature/mapa/
MapaViewModel.kt
ui/MapaScreen.kt
core/
network/
database/
ui/theme/
Con esta estructura, si vuelvo al proyecto después de dos semanas y necesito tocar algo relacionado con puntos guardados, entro en feature/puntos y ahí está todo lo relevante: la lógica, el acceso a datos y la pantalla. No tengo que reconstruir mentalmente qué archivo de viewmodels correspondía a qué pantalla de activities.
Separar claramente las capas dentro de cada función
Dentro de cada carpeta de función mantengo una separación de capas simple, sin sobrecargar el proyecto con patrones que no necesito en algo de este tamaño:
- UI: composables o vistas, sin lógica de negocio.
- ViewModel: estado de la pantalla y orquestación de acciones del usuario.
- Repository: acceso a datos, ya sea base de datos local o red.
- Data: entidades y modelos de datos.
Esta separación no es dogma de arquitectura limpia por sí misma; la mantengo porque me permite responder rápido a una pregunta muy concreta cuando vuelvo al proyecto: "¿este bug es de cómo se pinta la pantalla, de cómo se gestiona el estado, o de cómo se leen los datos?". Con las capas mezcladas, esa pregunta es mucho más difícil de responder sin releer todo el archivo.
Un ejemplo práctico: retomar una función a medias
Hace unas semanas dejé a medias la función de filtrar puntos guardados por tipo de pesca. Al volver, en vez de intentar recordar de memoria en qué punto lo dejé, hice tres cosas que ahora repito siempre:
Primero, revisé el archivo PuntosViewModel.kt buscando cualquier función o variable con un TODO explícito, que es una convención que sigo: nunca dejo una función a medias sin un comentario // TODO: describiendo qué falta. Segundo, comprobé el estado de los tests de esa función concreta, porque si dejé un test fallando a propósito, ese test me dice exactamente qué comportamiento faltaba por implementar. Tercero, miré el último commit relacionado con feature/puntos para leer el mensaje de commit, que en mi caso siempre describe el "por qué" del cambio, no solo el "qué".
Con esos tres puntos de referencia (TODO en código, test pendiente, mensaje de commit) recuperé el contexto completo en menos de cinco minutos, sin tener que releer todo el módulo desde cero.
Convenciones que mantengo en todos mis proyectos
Además de la estructura de carpetas, sigo un puñado de convenciones pequeñas pero constantes:
- Cada
ViewModelexpone un único estado (data class UiState), nunca variables sueltas dispersas. - Los nombres de archivos de test siguen el patrón
NombreDeClaseTest.kt, ubicados en la misma ruta relativa que la clase que prueban. - Cualquier decisión de arquitectura no obvia queda registrada como ADR, algo que explico con más detalle en el artículo sobre qué es un ADR y por qué lo uso en proyectos pequeños.
- El archivo
README.mdde cada módulo importante explica en tres líneas qué hace, no cómo está implementado por dentro.
Errores que cometí y que ahora evito
El primer error fue no separar el código por función desde el principio, y migrar la estructura ya con el proyecto crecido, lo cual fue más costoso de lo necesario. La lección aprendida es que la estructura por función merece la pena desde el primer commit, aunque solo tengas dos pantallas.
El segundo error fue confiar en mi memoria en vez de en TODOs y mensajes de commit descriptivos. Escribir "fix bug" como mensaje de commit ahorra veinte segundos en el momento, pero cuesta minutos cuando intentas reconstruir qué bug era exactamente tres semanas después.
Un límite real de este enfoque: organizar por función funciona bien mientras las funciones estén razonablemente desacopladas entre sí. Cuando dos funciones comparten mucha lógica, forzar la separación por carpetas puede generar duplicación de código innecesaria. En esos casos prefiero extraer esa lógica compartida a core/, en vez de replicarla en cada feature solo por mantener la estructura limpia a toda costa.
Conclusión
Organizar un proyecto Android no es solo una cuestión estética ni de seguir "buenas prácticas" por seguirlas. Para quien desarrolla en ratos sueltos, es sobre todo una forma de proteger el tiempo: cada minuto que no se pierde reconstruyendo contexto es un minuto que se puede dedicar a escribir código nuevo. La estructura por función, las capas separadas y unas pocas convenciones de commits y TODOs no son perfectas, pero han reducido de forma notable el tiempo que tardo en volver a sentirme "dentro" del proyecto cada vez que lo retomo.
