Kotlin Lab

Aprender / Kontor tickets — 20 cambios guiados

Kontor tickets — 20 cambios guiados

Estos tickets son para practicar Android/Compose sobre un proyecto real. No son un tutorial paralelo: cada uno toca código que ya existe y te obliga a leer antes de escribir.

Cómo usarlos

  1. Elige un ticket por sesión.

  2. Lee los archivos implicados antes de tocar nada.

  3. Escribe o ajusta un test que falle. No cambies un test existente solo para que pase; si crees que está mal, explica por qué antes de tocarlo.

  4. Haz el cambio mínimo.

  5. Ejecuta los tests del módulo.

  6. Explica en 90 segundos qué cambiaste y qué límite tiene.

  7. Anota el ticket en notas/kontor-tickets.md.

No hagas varios tickets a la vez. No mezcles refactors. No hagas commit si no te lo han pedido.

Si no has tocado Compose nunca, lee antes docs/COMPOSE.md. Los tickets asumen que entiendes @Composable, remember, state hoisting y LazyColumn.

Antes de empezar

  • Abre /Users/rubengmlp/projects/kontor en Android Studio.

  • Comprueba que compila: ./gradlew :app:assembleDebug.

  • Ejecuta los tests de un módulo antes de tocarlo: ./gradlew :feature:accounts:testDebugUnitTest.

  • Usa un emulador con API 26 o superior.

  • Si un ticket toca UI, ejecuta la app y prueba el flujo a mano además de los tests.

  • No hagas commit de los cambios de un ticket junto con los de otro.

Orden recomendado

BloqueTicketsFoco
UI1–6Previews, composables, accesibilidad, skeleton, refresh
Estado7–10SavedStateHandle, filtros, draft, errores
Datos11–15Repositorio, pantalla de cuenta, fallos, refresh parcial, idempotencia
Calidad16–20UI test, debounce, animación, tema oscuro, Room

Tabla rápida

#TicketDificultadTiempo
1Previews de AccountsScreen1/530–45 min
2Extraer AccountCard con preview2/545–60 min
3Resumen de ingresos/gastos3/52–3 h
4Semántica de TransactionRow2/51 h
5Skeleton de carga2/51–2 h
6Pull to refresh3/51–2 h
7SavedStateHandle en filtros3/51–2 h
8Filtro por categoría3/52 h
9Persistir borrador de transferencia4/52–3 h
10Mapear errores de red4/52–3 h
11Transacciones por cuenta en repositorio2/51 h
12Pantalla de detalle de cuenta4/53–4 h
13Inyectar fallos en el backend demo3/52 h
14Refresh parcial resiliente5/53–4 h
15Idempotency key en transferencias5/53–4 h
16UI test de validación de transferencia3/51–2 h
17Debounce en búsqueda4/52 h
18AnimatedContent entre pasos2/51 h
19Auditoría de tema oscuro2/51–2 h
20Tests de DAO con Room in-memory4/52–3 h

Ticket 1 — Previews de AccountsScreen

Objetivo: ver los estados de la pantalla sin ejecutar la app ni conectar Hilt.

Archivos

  • feature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsScreen.kt

  • Crea feature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsPreviewData.kt

Pasos

  1. Crea datos de preview: dos cuentas y tres transacciones.

  2. Añade @Preview para:

    • loading;

    • content;

    • error con cuentas cacheadas;

    • sin cuentas;

    • balances ocultos.

  3. Envuelve cada preview en KontorTheme.

  4. No uses hiltViewModel() dentro del preview.

Criterio de hecho

  • [ ] Los previews compilan.

  • [ ] Se ven los cinco estados.

  • [ ] Ningún preview hace red.

  • [ ] ./gradlew :feature:accounts:compileDebugKotlin pasa.

Pista: AccountsScreen recibe state y callbacks; no necesitas ViewModel.

Stretch: preview con fontScale = 1.3f y uiMode = UI_MODE_NIGHT_YES.


Ticket 2 — Extraer AccountCard con preview

Objetivo: separar la tarjeta de cuenta del archivo gigante y hacerla fácil de inspeccionar.

Archivos

  • feature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsScreen.kt

Pasos

  1. Mueve AccountCard a AccountCard.kt en el mismo paquete.

  2. Hazla internal o pública según necesidad.

  3. Añade previews para GIRO, SAVINGS, CREDIT y hidden = true.

  4. No cambies el diseño todavía.

Criterio de hecho

  • [ ] AccountsScreen.kt es más corto.

  • [ ] La tarjeta se ve igual.

  • [ ] Hay previews por tipo.

  • [ ] No hay imports muertos.

Pista: no la muevas a core:designsystem todavía: depende de Account, y el design system no depende del dominio.

Stretch: añade un parámetro onClick y úsalo para el ticket 12.


Ticket 3 — Resumen de ingresos y gastos

Objetivo: añadir un resumen de actividad a la home usando datos reales.

Archivos

  • feature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsViewModel.kt

  • feature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsScreen.kt

  • feature/accounts/src/test/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsViewModelTest.kt

Pasos

  1. Añade totalIncomeCents y totalExpenseCents a AccountsUiState.

  2. Observa repository.observeTransactions() en el combine.

  3. Calcula ingresos (amountCents > 0) y gastos (amountCents < 0, en valor absoluto).

  4. Añade una KontorCard con “Money in”, “Money out” y neto.

  5. Añade un test con transacciones positivas y negativas.

Criterio de hecho

  • [ ] El estado contiene los tres totales.

  • [ ] La UI los muestra formateados con Money.

  • [ ] El test falla si cambias el signo.

  • [ ] No añades un DAO nuevo para algo que ya puedes calcular en el ViewModel.

Pista: sumOf y filter son suficientes. No necesitas un nuevo DAO.

Stretch: calcula solo el mes actual con YearMonth.now() y añade un test con fechas fijas.


Ticket 4 — Semántica de TransactionRow

Objetivo: que TalkBack lea una transacción como una sola unidad comprensible.

Archivos

  • feature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/TransactionRow.kt

  • feature/accounts/src/androidTest/kotlin/com/rubengmlp/kontor/feature/accounts/TransactionRowTest.kt

Pasos

  1. Añade Modifier.semantics(mergeDescendants = true) a la fila.

  2. Define una descripción útil: título, categoría, importe y fecha.

  3. Si el importe está oculto, la descripción debe decir “Amount hidden”.

  4. Añade un test de Compose con onNodeWithContentDescription(...).

Criterio de hecho

  • [ ] TalkBack no lee cinco nodos sueltos.

  • [ ] La fila sigue siendo clickable.

  • [ ] El test cubre importe visible y oculto.

  • [ ] No duplicas la descripción del AmountText de forma confusa.

Pista: clearAndSetSemantics es más agresivo; úsalo solo si sabes qué estás ocultando.

Stretch: añade Role.Button y un stateDescription para “pending”.


Ticket 5 — Skeleton de carga

Objetivo: sustituir el spinner por un esqueleto que mantenga la estructura de la home.

Archivos

  • feature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsScreen.kt

  • core/designsystem/src/main/kotlin/com/rubengmlp/kontor/core/designsystem/components/KontorComponents.kt

Pasos

  1. Crea AccountsSkeleton con bloques del tamaño del hero, las tarjetas y las filas.

  2. Usa rememberInfiniteTransition para animar la alpha entre 0.35 y 0.7.

  3. Úsalo cuando state.isLoading && state.accounts.isEmpty().

  4. Añade preview.

Criterio de hecho

  • [ ] No hay salto brusco al cargar.

  • [ ] El skeleton respeta el tema claro/oscuro.

  • [ ] No bloquea la UI.

  • [ ] El preview se ve sin datos.

Pista: no hace falta una librería de shimmer. Una animación de alpha basta.

Stretch: añade Modifier.semantics { contentDescription = "Loading accounts" }.


Ticket 6 — Pull to refresh

Objetivo: permitir refrescar la home con el gesto nativo.

Archivos

  • feature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsScreen.kt

Pasos

  1. Envuelve AccountsContent en PullToRefreshBox.

  2. Usa isRefreshing = state.isRefreshing y onRefresh = onRefresh.

  3. Mantén el botón de refresh existente.

  4. Comprueba que no se lanzan dos refrescos seguidos.

Criterio de hecho

  • [ ] El gesto funciona en el emulador.

  • [ ] El indicador desaparece al terminar.

  • [ ] El botón sigue funcionando.

  • [ ] No hay crash si no hay cuentas.

Pista: puede requerir @OptIn(ExperimentalMaterial3Api::class).

Stretch: añade un test de UI que invoque onRefresh y verifique el callback.


Ticket 7 — SavedStateHandle en filtros

Objetivo: que la búsqueda y el filtro sobrevivan a la muerte de proceso.

Archivos

  • feature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/TransactionsViewModel.kt

  • feature/accounts/src/test/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsViewModelTest.kt

Pasos

  1. Inyecta SavedStateHandle en TransactionsViewModel.

  2. Inicializa FilterState desde savedStateHandle.

  3. Guarda query y filter.name en cada cambio.

  4. clearFilters() debe limpiar también el estado guardado.

  5. Añade un test que cree un segundo ViewModel con el mismo handle.

Criterio de hecho

  • [ ] El query sobrevive.

  • [ ] El filtro sobrevive.

  • [ ] clearFilters() deja el estado limpio.

  • [ ] No guardas objetos no serializables.

Pista: guarda el enum como String y conviértelo con TransactionFilter.valueOf(...).

Stretch: guarda también la posición del scroll con rememberSaveable.


Ticket 8 — Filtro por categoría

Objetivo: filtrar la actividad por TransactionCategory, combinable con búsqueda y dirección.

Archivos

  • feature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/TransactionsViewModel.kt

  • feature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/TransactionsScreen.kt

  • feature/accounts/src/test/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsViewModelTest.kt

Pasos

  1. Añade category: TransactionCategory? a FilterState y TransactionsUiState.

  2. Filtra por categoría después de query y dirección.

  3. Añade chips o un dropdown con “All” y las categorías.

  4. Añade un test que combine SOFTWARE con “Money out”.

  5. clearFilters() debe limpiar la categoría.

Criterio de hecho

  • [ ] Los filtros se combinan, no se pisan.

  • [ ] La UI indica la categoría activa.

  • [ ] Hay un estado vacío correcto.

  • [ ] El test cubre una categoría sin resultados.

Pista: TransactionCategory.entries te da todas las categorías; no escribas la lista a mano.

Stretch: añade contadores por categoría.


Ticket 9 — Persistir borrador de transferencia

Objetivo: que el usuario no pierda lo que escribió al rotar o al morir el proceso.

Archivos

  • feature/transfer/src/main/kotlin/com/rubengmlp/kontor/feature/transfer/TransferViewModel.kt

  • feature/transfer/src/test/kotlin/com/rubengmlp/kontor/feature/transfer/TransferViewModelTest.kt

Pasos

  1. Inyecta SavedStateHandle.

  2. Guarda recipientName, iban, amountInput, reference y fromAccountId.

  3. Restaura el estado inicial del formulario.

  4. No guardes receipt, isSubmitting ni errors.

  5. Limpia el borrador al completar o al empezar una transferencia nueva.

  6. Añade un test con dos ViewModels y el mismo handle.

Criterio de hecho

  • [ ] El borrador sobrevive.

  • [ ] El recibo no reaparece.

  • [ ] El estado isSubmitting no se restaura.

  • [ ] startNewTransferIfComplete() limpia el borrador.

Pista: usa claves constantes para no equivocarte con strings repetidos.

Stretch: combínalo con el ticket 15 para persistir también la idempotency key.


Ticket 10 — Mapear errores de red

Objetivo: que la UI reciba mensajes útiles y no java.net.SocketTimeoutException.

Archivos

  • core/domain/src/main/kotlin/com/rubengmlp/kontor/core/domain/ (nuevo BankError.kt)

  • core/data/src/main/kotlin/com/rubengmlp/kontor/core/data/repository/DefaultBankRepository.kt

  • feature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsViewModel.kt

  • Tests de dominio o data.

Pasos

  1. Crea un sealed interface BankError con Network, Http(code, message), Validation, Unknown.

  2. Mapea IOException, HttpException y errores de serialización.

  3. Haz que el repositorio devuelva Result con un BankException tipado o un BankError.

  4. En el ViewModel, convierte el error en un mensaje de usuario.

  5. Añade tests para timeout, 401, 500 y error desconocido.

Criterio de hecho

  • [ ] La UI no muestra stack traces.

  • [ ] 401 se distingue de 500.

  • [ ] Un timeout dice algo como “No connection”.

  • [ ] El mapeo está testeado sin Android.

Pista: no metas Retrofit en el ViewModel. El mapeo vive en data/domain.

Stretch: añade un errorCode para telemetría.


Ticket 11 — Transacciones por cuenta en el repositorio

Objetivo: exponer la consulta que ya existe en el DAO.

Archivos

  • core/domain/src/main/kotlin/com/rubengmlp/kontor/core/domain/repository/BankRepository.kt

  • core/data/src/main/kotlin/com/rubengmlp/kontor/core/data/repository/DefaultBankRepository.kt

  • feature/accounts/src/test/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsViewModelTest.kt

  • feature/transfer/src/test/kotlin/com/rubengmlp/kontor/feature/transfer/TransferViewModelTest.kt

Pasos

  1. Añade observeTransactionsForAccount(accountId: String): Flow<List<Transaction>>.

  2. Impleméntalo con transactionDao.observeForAccount(accountId).

  3. Actualiza todos los fakes de BankRepository.

  4. Añade un test que devuelva solo las transacciones de una cuenta.

Criterio de hecho

  • [ ] El método existe en la interfaz.

  • [ ] Todos los fakes compilan.

  • [ ] El test cubre una cuenta sin transacciones.

  • [ ] No duplicas la consulta SQL.

Pista: el DAO ya tiene observeForAccount; este ticket es de conexión, no de SQL.

Stretch: úsalo en el ticket 12.


Ticket 12 — Pantalla de detalle de cuenta

Objetivo: abrir una cuenta y ver su información y sus movimientos.

Archivos

  • feature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/AccountDetailScreen.kt (nuevo)

  • feature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/AccountDetailViewModel.kt (nuevo)

  • feature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsScreen.kt

  • app/src/main/kotlin/com/rubengmlp/kontor/navigation/Routes.kt

  • app/src/main/kotlin/com/rubengmlp/kontor/KontorApp.kt

Pasos

  1. Añade @Serializable data class AccountDetail(val id: String) en Routes.kt, junto a TransactionDetail.

  2. Crea un ViewModel con SavedStateHandle que combine observeAccount(id) y observeTransactionsForAccount(id).

  3. Crea la pantalla con top bar, tarjeta de cuenta, lista de movimientos y estados loading/empty/error.

  4. Haz AccountCard clickable y navega.

  5. Añade tests del ViewModel.

Criterio de hecho

  • [ ] Se navega desde la home.

  • [ ] El back vuelve a la home.

  • [ ] La pantalla no conoce el repositorio directamente.

  • [ ] Hay un test de estado con cuenta y sin cuenta.

  • [ ] La lista usa TransactionRow.

Pista: reutiliza KontorTopBar, AccountCard y TransactionRow; no reinventes.

Stretch: añade un botón “Transferir desde esta cuenta” que precargue la cuenta.


Ticket 13 — Inyectar fallos en el backend demo

Objetivo: poder probar estados de error sin desconectar el WiFi.

Archivos

  • core/data/src/main/kotlin/com/rubengmlp/kontor/core/data/remote/DemoApiInterceptor.kt

  • feature/profile/src/main/kotlin/com/rubengmlp/kontor/feature/profile/ProfileScreen.kt

  • feature/profile/build.gradle.kts

  • core/data/src/test/... (test puro del selector de fallo)

Pasos

  1. Crea DemoFailureMode con None, Http500, Network, Slow.

  2. Añade un objeto DemoFailure con @Volatile var mode.

  3. En el interceptor, aplica el modo antes de responder.

  4. Activa buildConfig = true en feature/profile, añade implementation(project(":core:data")) y muestra los controles solo en BuildConfig.DEBUG. En producción, esto viviría en un módulo debug aparte; aquí lo hacemos simple y lo justificamos.

  5. Añade un test unitario para la decisión de fallo.

Criterio de hecho

  • [ ] Puedes provocar un 500 desde la app.

  • [ ] Puedes provocar un timeout.

  • [ ] El modo se resetea al reiniciar.

  • [ ] En release no aparece el control.

  • [ ] La app muestra el error mapeado del ticket 10.

Pista: no uses un if (debug) disperso; centraliza el modo.

Stretch: añade un botón “Reset demo data” que borre la base de datos.


Ticket 14 — Refresh parcial resiliente

Objetivo: si falla una de las dos llamadas de refresh, no perder la que sí funcionó.

Archivos

  • core/data/src/main/kotlin/com/rubengmlp/kontor/core/data/repository/DefaultBankRepository.kt

  • Tests de DefaultBankRepository con fakes.

Pasos

  1. Cambia coroutineScope por supervisorScope.

  2. Lanza cuentas y transacciones en paralelo.

  3. Persiste cada resultado que llegue bien.

  4. Devuelve un error agregado si alguna falla.

  5. Añade un test donde transactions falla y accounts sí se guarda.

Criterio de hecho

  • [ ] Un fallo de transactions no borra las cuentas.

  • [ ] El ViewModel muestra un error.

  • [ ] No se oculta el fallo.

  • [ ] El test no depende de una red real.

Pista: supervisorScope no cancela a los hermanos; aun así, maneja cada await con cuidado.

Stretch: crea un RefreshResult con accountsUpdated, transactionsUpdated y errors.


Ticket 15 — Idempotency key en transferencias

Objetivo: que reintentar una transferencia no la ejecute dos veces.

Archivos

  • core/domain/src/main/kotlin/com/rubengmlp/kontor/core/domain/model/Transfer.kt

  • core/data/src/main/kotlin/com/rubengmlp/kontor/core/data/remote/BankDtos.kt

  • feature/transfer/src/main/kotlin/com/rubengmlp/kontor/feature/transfer/TransferViewModel.kt

  • Tests de ViewModel y DTO.

Pasos

  1. Añade clientRequestId: String a TransferRequest.

  2. Genera un UUID al iniciar el borrador y guárdalo en el estado.

  3. Reutiliza la misma clave al reintentar.

  4. Genera una nueva al empezar una transferencia nueva.

  5. Envía la clave en el DTO.

  6. Añade un test que simule un fallo y un reintento, y verifique que la clave no cambia.

Criterio de hecho

  • [ ] El primer intento y el reintento comparten clave.

  • [ ] Una transferencia nueva tiene otra clave.

  • [ ] La clave sobrevive a rotación si hiciste el ticket 9.

  • [ ] Documentas que el backend demo no la enforce.

Pista: la idempotencia real se implementa en el servidor; en el cliente solo garantizas estabilidad.

Stretch: añade un IdempotencyStore en memoria para tests.


Ticket 16 — UI test de validación de transferencia

Objetivo: probar la pantalla sin Hilt ni backend.

Archivos

  • feature/transfer/src/androidTest/kotlin/com/rubengmlp/kontor/feature/transfer/TransferScreenTest.kt

Pasos

  1. Crea una createComposeRule.

  2. Renderiza TransferScreen con un TransferUiState que tenga errores.

  3. Verifica que se muestran los mensajes.

  4. Verifica que “Review transfer” invoca el callback.

  5. Añade un caso con isSubmitting = true.

Criterio de hecho

  • [ ] El test corre en el emulador.

  • [ ] No usa Hilt.

  • [ ] No hace red.

  • [ ] Cubre error y estado normal.

  • [ ] ./gradlew :feature:transfer:connectedDebugAndroidTest pasa.

Pista: los mensajes salen de state.errors, así que el test decide el texto: construye el estado con errors = TransferValidation(recipientError = "Enter the recipient's name") y luego onNodeWithText("Enter the recipient's name").assertIsDisplayed().

Stretch: añade un test de accesibilidad con assertHasClickAction().


Ticket 17 — Debounce en búsqueda

Objetivo: no filtrar en cada tecla; esperar una pausa corta.

Archivos

  • feature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/TransactionsViewModel.kt

  • feature/accounts/src/test/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsViewModelTest.kt

Pasos

  1. Separa el flujo de query del flujo de filtros.

  2. Aplica debounce(300) solo a la query.

  3. Mantén los chips inmediatos.

  4. Añade un test con advanceTimeBy(299) y advanceTimeBy(1).

  5. Comprueba que clearFilters() no espera 300 ms.

Criterio de hecho

  • [ ] Escribir rápido no dispara diez filtros.

  • [ ] Los chips responden al instante.

  • [ ] El test es determinista con runTest.

  • [ ] No usas Thread.sleep.

Pista: @OptIn(FlowPreview::class) para debounce.

Stretch: añade distinctUntilChanged() y un test de query repetida.


Ticket 18 — AnimatedContent entre pasos

Objetivo: que el cambio de formulario a revisión no sea un corte seco.

Archivos

  • feature/transfer/src/main/kotlin/com/rubengmlp/kontor/feature/transfer/TransferScreen.kt

Pasos

  1. Envuelve el when (state.step) en AnimatedContent.

  2. Usa una transición de fade + slide horizontal.

  3. Mantén el targetState como state.step.

  4. Comprueba que “Edit details” vuelve sin perder datos.

Criterio de hecho

  • [ ] La transición se ve suave.

  • [ ] No hay saltos de layout.

  • [ ] El back sigue funcionando.

  • [ ] No animas el estado SUCCESS si no quieres.

Pista: togetherWith y SizeTransform pueden ayudarte, pero no son obligatorios.

Stretch: anima el progreso de los pasos con animateColorAsState.


Ticket 19 — Auditoría de tema oscuro

Objetivo: que todas las pantallas se lean bien en oscuro y con fuente grande.

Archivos

  • core/designsystem/src/main/kotlin/com/rubengmlp/kontor/core/designsystem/theme/Theme.kt

  • core/designsystem/src/main/kotlin/com/rubengmlp/kontor/core/designsystem/theme/Color.kt

  • Previews de las pantallas.

Pasos

  1. Añade previews con uiMode = UI_MODE_NIGHT_YES.

  2. Añade un preview con fontScale = 1.3f.

  3. Busca colores hardcodeados (Color.White, Color.Black) en superficies.

  4. Corrige contrastes con MaterialTheme.colorScheme.

  5. Revisa AmountText, StatusPill, HeroCard y KontorTopBar.

Criterio de hecho

  • [ ] Ningún texto blanco sobre fondo claro.

  • [ ] Los importes se leen en oscuro.

  • [ ] Las tarjetas no pierden bordes.

  • [ ] Hay previews de dark y fontScale.

  • [ ] No rompes el tema claro.

Pista: no cambies la paleta entera; corrige los casos concretos.

Stretch: añade un test de contraste con Paparazzi o Roborazzi.


Ticket 20 — Tests de DAO con Room in-memory

Objetivo: probar la capa de datos sin emulador de red ni backend.

Archivos

  • core/data/build.gradle.kts

  • core/data/src/androidTest/kotlin/com/rubengmlp/kontor/core/data/local/DaoTest.kt (nuevo)

Pasos

  1. Añade testInstrumentationRunner en defaultConfig y dependencias androidTestImplementation de Room testing y AndroidX Test.

  2. Crea una base de datos in-memory con Room.inMemoryDatabaseBuilder.

  3. Prueba AccountDao.upsertAll y observeAll.

  4. Prueba TransactionDao.observeForAccount.

  5. Prueba que TransferDao.applyTransfer actualiza saldo e inserta la transacción.

Criterio de hecho

  • [ ] Los tests corren en el emulador.

  • [ ] La base se cierra en @After.

  • [ ] No usan la base real de la app.

  • [ ] Cubren una consulta con lista vacía.

  • [ ] ./gradlew :core:data:connectedDebugAndroidTest pasa.

Pista: allowMainThreadQueries() solo en tests; en producción no.

Stretch: añade un test de migración con MigrationTestHelper.


Cuando termines los 20

No habrás terminado Android. Habrás tocado UI, estado, datos, errores, tests y accesibilidad en un proyecto real. Eso es exactamente lo que se espera de alguien que transiciona desde Flutter con base seria.

Siguiente:

  1. Elige un capstone de docs/PROJECTS.md.

  2. Haz un code review de tus tickets con docs/ASSESSMENT.md.

  3. Explica tres tickets en inglés, en 2 minutos cada uno.

  4. Contribuye a un repo open source.

Fuente: docs/KONTOR_TICKETS.md