Kotlin Lab

Aprender / Los niveles del laboratorio

Los niveles del laboratorio

Cada nivel tiene un objetivo, una lista de ejercicios, errores típicos y una definición de “hecho”. No pases al siguiente nivel por terminar un ejercicio: pasa cuando puedas explicar el nivel.

Nivel 1 — Sintaxis y control de flujo

Objetivo: escribir funciones pequeñas sin pelear con la sintaxis.

EjercicioConceptoPista
greetString templates"Hola, $name"
isEvenOperador módulovalue % 2 == 0
maxOfThreeExpresionesmaxOf(a, b, c)
fizzBuzzwhenOrdena las condiciones de 15, 3, 5
safeDivideNullableif (b == 0) null else a / b
parsePositiveInttoIntOrNull + takeIftrim() antes de parsear

Errores típicos:

  • Usar if/else anidados en vez de when.

  • Devolver 0 en vez de null para un error.

  • Olvidar que Int? y Int son tipos distintos.

  • Escribir return en una función de expresión.

Definición de hecho: los 6 tests pasan y puedes explicar la diferencia entre Int e Int?.


Nivel 2 — Colecciones y lambdas

Objetivo: transformar datos como lo harías con where, map, groupBy y fold en Dart.

EjercicioConceptoPista
totalCentssumOfNo uses un for si sumOf se lee mejor
filterByQueryfilter + contains(ignoreCase = true)Query vacía devuelve todo
groupByCategorygroupByEl resultado es Map<String, List<Transaction>>
topCounterpartiesgroupBy + mapValues + sortedWithOrdena por suma descendente; desempata por nombre
formatCentsLógica con LongSepara unidades y decimales; no uses Double
wordCountRegex + groupingBy + eachCountIgnora signos y mayúsculas

Errores típicos:

  • Usar Double para dinero.

  • Mutar una lista mientras la recorres.

  • Ordenar antes de agrupar.

  • Olvidar el caso de limit = 0.

  • Formatear con la configuración local del sistema y obtener un resultado distinto en CI.

Definición de hecho: los 6 tests pasan y puedes explicar por qué List y MutableList son interfaces distintas.


Nivel 3 — Clases y dominio

Objetivo: modelar dinero, cuentas y transferencias con tipos que impidan errores.

EjercicioConceptoPista
Money.plusoperator funSuma céntimos
Money.minusoperator funResta céntimos
Money.isPositivePredicadocents > 0
Account.availableComposiciónbalance - reserved
validateTransfersealed interface + buildListAcumula todos los motivos, no solo el primero

Reglas de validateTransfer:

  1. Importe mayor que cero.

  2. IBAN con 2 letras, 2 dígitos y 11–30 caracteres alfanuméricos.

  3. Referencia de 140 caracteres como máximo.

  4. Importe menor o igual que el saldo disponible.

Errores típicos:

  • Validar con if y devolver el primer error.

  • Usar String para el estado en vez de sealed.

  • Mutar Money en vez de devolver una copia.

  • No normalizar el IBAN con trim().uppercase().

  • Confundir saldo con saldo disponible.

Definición de hecho: los 8 tests pasan y puedes dibujar el flujo de una transferencia válida e inválida.


Nivel 4 — Genéricos y sealed types

Objetivo: escribir APIs reutilizables sin perder tipos.

EjercicioConceptoPista
ApiResult.mapGenéricos + sealedSuccess transforma; Failure se propaga
ApiResult.getOrNullPattern matchingwhen sobre los dos casos
Repository.findByIdInterfaz genéricaitems[id]
Repository.findAllOrden de inserciónlinkedMapOf
Repository.saveReemplazo por iditems[idOf(entity)] = entity
Repository.deleteBooleanoremove(id) != null
Box.mapCovarianzaBox(transform(value))
medianComparable<T>Ordena y elige el índice central inferior

Errores típicos:

  • Usar Any en vez de un parámetro de tipo.

  • Hacer ApiResult invariante y pelear con el compilador.

  • Implementar findAll devolviendo la referencia interna mutable.

  • Confundir mediana inferior con media.

  • Usar !! en getOrNull.

Definición de hecho: los 7 tests pasan y puedes explicar por qué ApiResult.Failure es ApiResult<Nothing>.


Nivel 5 — Kotlin avanzado

Objetivo: leer y escribir código Kotlin que no parece Dart traducido.

EjercicioConceptoPista
typeNameinline + reifiedT::class.simpleName
fibonaccisequence + yieldDos variables y un bucle infinito
AccountBuilder.buildBuilderDevuelve Account(id, name, balanceCents)
accountLambda con receptorAccountBuilder().apply(block).build()
readOnlyListDelegación byReadOnlyList(values.toList())

Errores típicos:

  • Usar List donde hace falta MutableList o al revés.

  • Creer que sequence es una lista; es perezosa.

  • Usar apply cuando let se lee mejor.

  • Abusar de DSLs para un caso de dos líneas.

  • Olvidar que reified solo funciona en funciones inline.

Definición de hecho: los 4 tests pasan y puedes explicar cuándo no usar un DSL.


Nivel 6 — Coroutines y Flow

Objetivo: pasar de Future/Stream a suspend/Flow con tests deterministas.

EjercicioConceptoPista
loadAccountsuspend + delaySimula latencia y devuelve la cuenta
retryReintentosGuarda el último error y relanza al agotar
accountUpdatesflow { emit(...) }Tres emisiones
searchAccountsFlow.mapFiltra por nombre con ignoreCase = true

Errores típicos:

  • Usar Thread.sleep en una coroutine.

  • Lanzar GlobalScope.

  • Capturar CancellationException y seguir.

  • Testear con delay real en vez de runTest.

  • Confundir Flow con StateFlow.

Definición de hecho: los 6 tests pasan y puedes explicar la diferencia entre launch y async.


Nivel 7 — Idioms de mid Android

Objetivo: escribir Kotlin idiomático y entender qué sobrevive a process death. Acompaña a docs/ANDROID_FLUENCY.md §1.4, §3 y §4.

EjercicioConceptoPista
Iban.parsevalue class + smart constructorfilterNot { it.isWhitespace() }, uppercase(), takeIf, ?.let(::Iban)
Iban.maskedExtensióntake(4) y takeLast(4)
normalizeReference?. + takeIftrim(), isNotEmpty(), take(140)
transferArgsapply (≈ cascade .. de Dart)Como Intent().apply { putExtra(…) }
alsoLogalsoEfecto lateral y devuelve this
CountingAccountRepositoryDelegación de clase bySolo sobrescribes accounts()
ExchangeRates.ratesby lazySustituye el getter por val rates by lazy(load)
runSuspendCatchingrunCatching + cancelacióncatch (e: CancellationException) { throw e } antes del Throwable
SavedStateHandleoperator get/set + StateFlowflows.getOrPut(key) { … }
savedDelegado de propiedadobject : ReadWriteProperty<Any?, T>, clave = property.name
canSubmitJuntarlo todoIban.parse(...) != null y toLongOrNull()

Errores típicos:

  • Usar typealias esperando seguridad de tipos: Cents acepta cualquier Long.

  • Hacer público el constructor de Iban y validar fuera: el tipo deja de garantizar nada.

  • ?.let { } ?: run { } como if/else.

  • Usar runCatching en código suspend: el test de cancelación lo demuestra.

  • Creer que el ViewModel sobrevive a process death: solo sobrevive lo que está en el SavedStateHandle.

Extra: añade la validación real del IBAN (mod 97) a Iban.parse y escribe un test con un dígito de control incorrecto.

Definición de hecho: los 15 tests pasan y puedes explicar, sin mirar, por qué a cancelled coroutine does not keep running fallaría con runCatching.


Nivel 8 — ViewModel de una transferencia

Objetivo: implementar el ViewModel de una transferencia como en producción y entender, con tests, tres cosas que se preguntan siempre: qué pasa con viewModelScope al salir de la pantalla, qué sobrevive a process death y por qué la idempotency key evita el doble cobro. Lee antes docs/ANDROID_BASICS.md §5–§7 y §11.

EjercicioConceptoPista
ViewModel.clearCiclo de vida del ViewModelviewModelScope.cancel() y luego onCleared()
onAmountChangeSavedStateHandleCopia onIbanChange
onReviewValidación + idempotency keycurrent.idempotencyKey ?: newIdempotencyKey(); guárdala en savedState
onEditNueva intención de pagosavedState.remove(KEY_IDEMPOTENCY)
onConfirmScope correcto + incertidumbreappScope.launch, no viewModelScope; timeout → Unknown
onRetryReintento idempotenteEs onConfirm() con la misma key

Errores típicos:

  • Lanzar el envío en viewModelScope: el test leaving the screen… falla porque la transferencia se cancela.

  • Generar la key en onConfirm: cada reintento sería una transferencia distinta.

  • Tratar el timeout como Failed: el usuario reintentaría sin saber que ya pagó.

  • Guardar en savedState la lista de cuentas o el recibo entero: solo lo pequeño que genera el usuario.

Extra: tras process death con una key guardada y sin recibo, el estado debería empezar en Unknown y consultar al servidor por la key. Escribe el test primero.

Definición de hecho: los 12 tests pasan y puedes explicar en voz alta, sin mirar, el test retrying after a timeout reuses the key and the server pays only once. Compáralo con TransferViewModel.onConfirm() de Kontor: ¿qué cambiarías allí?


Después de los niveles

Cuando termines el nivel 8, no has terminado Kotlin. Has terminado la base.

Siguiente:

  1. docs/PROJECTS.md: capstones.

  2. docs/KONTOR_TICKETS.md: Android/Compose real.

  3. docs/ASSESSMENT.md: rúbrica y banco único de preguntas.

  4. docs/CURRICULUM.md: fases 4–6.

Cómo saber que un nivel está realmente hecho

No es “los tests pasan”. Es:

  • los tests pasan;

  • puedes explicar cada función sin mirar;

  • puedes escribir una variante nueva sin copiar;

  • sabes qué errores evitaste;

  • puedes decir cuándo no usar ese concepto.

Si falla una de esas, repite el nivel con variaciones.

Fuente: docs/LEVELS.md