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
Elige un ticket por sesión.
Lee los archivos implicados antes de tocar nada.
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.
Haz el cambio mínimo.
Ejecuta los tests del módulo.
Explica en 90 segundos qué cambiaste y qué límite tiene.
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/kontoren 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
| Bloque | Tickets | Foco |
|---|---|---|
| UI | 1–6 | Previews, composables, accesibilidad, skeleton, refresh |
| Estado | 7–10 | SavedStateHandle, filtros, draft, errores |
| Datos | 11–15 | Repositorio, pantalla de cuenta, fallos, refresh parcial, idempotencia |
| Calidad | 16–20 | UI test, debounce, animación, tema oscuro, Room |
Tabla rápida
| # | Ticket | Dificultad | Tiempo |
|---|---|---|---|
| 1 | Previews de AccountsScreen | 1/5 | 30–45 min |
| 2 | Extraer AccountCard con preview | 2/5 | 45–60 min |
| 3 | Resumen de ingresos/gastos | 3/5 | 2–3 h |
| 4 | Semántica de TransactionRow | 2/5 | 1 h |
| 5 | Skeleton de carga | 2/5 | 1–2 h |
| 6 | Pull to refresh | 3/5 | 1–2 h |
| 7 | SavedStateHandle en filtros | 3/5 | 1–2 h |
| 8 | Filtro por categoría | 3/5 | 2 h |
| 9 | Persistir borrador de transferencia | 4/5 | 2–3 h |
| 10 | Mapear errores de red | 4/5 | 2–3 h |
| 11 | Transacciones por cuenta en repositorio | 2/5 | 1 h |
| 12 | Pantalla de detalle de cuenta | 4/5 | 3–4 h |
| 13 | Inyectar fallos en el backend demo | 3/5 | 2 h |
| 14 | Refresh parcial resiliente | 5/5 | 3–4 h |
| 15 | Idempotency key en transferencias | 5/5 | 3–4 h |
| 16 | UI test de validación de transferencia | 3/5 | 1–2 h |
| 17 | Debounce en búsqueda | 4/5 | 2 h |
| 18 | AnimatedContent entre pasos | 2/5 | 1 h |
| 19 | Auditoría de tema oscuro | 2/5 | 1–2 h |
| 20 | Tests de DAO con Room in-memory | 4/5 | 2–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.ktCrea
feature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsPreviewData.kt
Pasos
Crea datos de preview: dos cuentas y tres transacciones.
Añade
@Previewpara:loading;
content;
error con cuentas cacheadas;
sin cuentas;
balances ocultos.
Envuelve cada preview en
KontorTheme.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:compileDebugKotlinpasa.
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
Mueve
AccountCardaAccountCard.kten el mismo paquete.Hazla
internalo pública según necesidad.Añade previews para
GIRO,SAVINGS,CREDITyhidden = true.No cambies el diseño todavía.
Criterio de hecho
[ ]
AccountsScreen.ktes 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.ktfeature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsScreen.ktfeature/accounts/src/test/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsViewModelTest.kt
Pasos
Añade
totalIncomeCentsytotalExpenseCentsaAccountsUiState.Observa
repository.observeTransactions()en elcombine.Calcula ingresos (
amountCents > 0) y gastos (amountCents < 0, en valor absoluto).Añade una
KontorCardcon “Money in”, “Money out” y neto.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.ktfeature/accounts/src/androidTest/kotlin/com/rubengmlp/kontor/feature/accounts/TransactionRowTest.kt
Pasos
Añade
Modifier.semantics(mergeDescendants = true)a la fila.Define una descripción útil: título, categoría, importe y fecha.
Si el importe está oculto, la descripción debe decir “Amount hidden”.
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
AmountTextde 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.ktcore/designsystem/src/main/kotlin/com/rubengmlp/kontor/core/designsystem/components/KontorComponents.kt
Pasos
Crea
AccountsSkeletoncon bloques del tamaño del hero, las tarjetas y las filas.Usa
rememberInfiniteTransitionpara animar la alpha entre 0.35 y 0.7.Úsalo cuando
state.isLoading && state.accounts.isEmpty().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
Envuelve
AccountsContentenPullToRefreshBox.Usa
isRefreshing = state.isRefreshingyonRefresh = onRefresh.Mantén el botón de refresh existente.
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.ktfeature/accounts/src/test/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsViewModelTest.kt
Pasos
Inyecta
SavedStateHandleenTransactionsViewModel.Inicializa
FilterStatedesdesavedStateHandle.Guarda
queryyfilter.nameen cada cambio.clearFilters()debe limpiar también el estado guardado.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.ktfeature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/TransactionsScreen.ktfeature/accounts/src/test/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsViewModelTest.kt
Pasos
Añade
category: TransactionCategory?aFilterStateyTransactionsUiState.Filtra por categoría después de query y dirección.
Añade chips o un dropdown con “All” y las categorías.
Añade un test que combine
SOFTWAREcon “Money out”.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.ktfeature/transfer/src/test/kotlin/com/rubengmlp/kontor/feature/transfer/TransferViewModelTest.kt
Pasos
Inyecta
SavedStateHandle.Guarda
recipientName,iban,amountInput,referenceyfromAccountId.Restaura el estado inicial del formulario.
No guardes
receipt,isSubmittingnierrors.Limpia el borrador al completar o al empezar una transferencia nueva.
Añade un test con dos ViewModels y el mismo handle.
Criterio de hecho
[ ] El borrador sobrevive.
[ ] El recibo no reaparece.
[ ] El estado
isSubmittingno 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/(nuevoBankError.kt)core/data/src/main/kotlin/com/rubengmlp/kontor/core/data/repository/DefaultBankRepository.ktfeature/accounts/src/main/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsViewModel.ktTests de dominio o data.
Pasos
Crea un
sealed interface BankErrorconNetwork,Http(code, message),Validation,Unknown.Mapea
IOException,HttpExceptiony errores de serialización.Haz que el repositorio devuelva
Resultcon unBankExceptiontipado o unBankError.En el ViewModel, convierte el error en un mensaje de usuario.
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.ktcore/data/src/main/kotlin/com/rubengmlp/kontor/core/data/repository/DefaultBankRepository.ktfeature/accounts/src/test/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsViewModelTest.ktfeature/transfer/src/test/kotlin/com/rubengmlp/kontor/feature/transfer/TransferViewModelTest.kt
Pasos
Añade
observeTransactionsForAccount(accountId: String): Flow<List<Transaction>>.Impleméntalo con
transactionDao.observeForAccount(accountId).Actualiza todos los fakes de
BankRepository.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.ktapp/src/main/kotlin/com/rubengmlp/kontor/navigation/Routes.ktapp/src/main/kotlin/com/rubengmlp/kontor/KontorApp.kt
Pasos
Añade
@Serializable data class AccountDetail(val id: String)enRoutes.kt, junto aTransactionDetail.Crea un ViewModel con
SavedStateHandleque combineobserveAccount(id)yobserveTransactionsForAccount(id).Crea la pantalla con top bar, tarjeta de cuenta, lista de movimientos y estados loading/empty/error.
Haz
AccountCardclickable y navega.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.ktfeature/profile/src/main/kotlin/com/rubengmlp/kontor/feature/profile/ProfileScreen.ktfeature/profile/build.gradle.ktscore/data/src/test/...(test puro del selector de fallo)
Pasos
Crea
DemoFailureModeconNone,Http500,Network,Slow.Añade un objeto
DemoFailurecon@Volatile var mode.En el interceptor, aplica el modo antes de responder.
Activa
buildConfig = trueenfeature/profile, añadeimplementation(project(":core:data"))y muestra los controles solo enBuildConfig.DEBUG. En producción, esto viviría en un módulodebugaparte; aquí lo hacemos simple y lo justificamos.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.ktTests de
DefaultBankRepositorycon fakes.
Pasos
Cambia
coroutineScopeporsupervisorScope.Lanza cuentas y transacciones en paralelo.
Persiste cada resultado que llegue bien.
Devuelve un error agregado si alguna falla.
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.ktcore/data/src/main/kotlin/com/rubengmlp/kontor/core/data/remote/BankDtos.ktfeature/transfer/src/main/kotlin/com/rubengmlp/kontor/feature/transfer/TransferViewModel.ktTests de ViewModel y DTO.
Pasos
Añade
clientRequestId: StringaTransferRequest.Genera un UUID al iniciar el borrador y guárdalo en el estado.
Reutiliza la misma clave al reintentar.
Genera una nueva al empezar una transferencia nueva.
Envía la clave en el DTO.
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
Crea una
createComposeRule.Renderiza
TransferScreencon unTransferUiStateque tenga errores.Verifica que se muestran los mensajes.
Verifica que “Review transfer” invoca el callback.
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:connectedDebugAndroidTestpasa.
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.ktfeature/accounts/src/test/kotlin/com/rubengmlp/kontor/feature/accounts/AccountsViewModelTest.kt
Pasos
Separa el flujo de query del flujo de filtros.
Aplica
debounce(300)solo a la query.Mantén los chips inmediatos.
Añade un test con
advanceTimeBy(299)yadvanceTimeBy(1).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
Envuelve el
when (state.step)enAnimatedContent.Usa una transición de fade + slide horizontal.
Mantén el
targetStatecomostate.step.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
SUCCESSsi 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.ktcore/designsystem/src/main/kotlin/com/rubengmlp/kontor/core/designsystem/theme/Color.ktPreviews de las pantallas.
Pasos
Añade previews con
uiMode = UI_MODE_NIGHT_YES.Añade un preview con
fontScale = 1.3f.Busca colores hardcodeados (
Color.White,Color.Black) en superficies.Corrige contrastes con
MaterialTheme.colorScheme.Revisa
AmountText,StatusPill,HeroCardyKontorTopBar.
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.ktscore/data/src/androidTest/kotlin/com/rubengmlp/kontor/core/data/local/DaoTest.kt(nuevo)
Pasos
Añade
testInstrumentationRunnerendefaultConfigy dependenciasandroidTestImplementationde Room testing y AndroidX Test.Crea una base de datos in-memory con
Room.inMemoryDatabaseBuilder.Prueba
AccountDao.upsertAllyobserveAll.Prueba
TransactionDao.observeForAccount.Prueba que
TransferDao.applyTransferactualiza 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:connectedDebugAndroidTestpasa.
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:
Elige un capstone de
docs/PROJECTS.md.Haz un code review de tus tickets con
docs/ASSESSMENT.md.Explica tres tickets en inglés, en 2 minutos cada uno.
Contribuye a un repo open source.