Kotlin Lab

Aprender / Evaluación y autoevaluación

Evaluación y autoevaluación

No te puntúes por “horas vistas”. Puntúate por lo que puedes hacer sin ayuda. Esta rúbrica está pensada para revisarla cada cuatro semanas, no para castigarte.

Cómo usarla

  1. Lee cada dimensión.

  2. Asigna un nivel 0–3 con una prueba concreta.

  3. Escribe la evidencia: un test, un archivo, una explicación grabada.

  4. Elige una dimensión para mejorar este mes.

  5. Vuelve a puntuar en cuatro semanas.

Rúbrica

Dimensión0 — No1 — Reconozco2 — Lo hago3 — Lo explico y decido
Sintaxis e idiomsNo compilo sin copiarEntiendo el código, pero necesito referenciasEscribo funciones y clases con documentaciónEscribo Kotlin idiomático y sé cuándo no usar un idiom
Tipos y nulabilidadUso Any y !!Entiendo String?, pero peleo con el compiladorModelo con tipos y evito !!Diseño APIs que hacen imposible el estado inválido
DominioTodo son String y BooleanSeparo clases, pero mezclo capasModelo Money, Iban, Transfer con testsDecido qué es entidad, value object, error y caso de uso
ColeccionesUso bucles para todoConozco map/filter, pero no los combinoTransformo datos con groupBy, sumOf, associateByElijo la estructura por coste y legibilidad
GenéricosEvito leerlosEntiendo List<T>Escribo Repository<T, ID> y ApiResult<T>Entiendo variance y sé cuándo una abstracción sobra
Coroutines y FlowUso GlobalScope o Thread.sleepEntiendo suspend, pero no la cancelaciónUso scopes, runTest, StateFlow y TurbineDiseño concurrencia, cancelación, reintentos y backpressure
Tests y debuggingCopio testsEscribo tests del camino felizCubro bordes y fallos, aíslo bugsDiseño la estrategia de tests y sé qué no testear
Android/ComposeNo sé por dónde empezarEntiendo un @ComposableHago pantallas con estados, navegación y ViewModelDecido state hoisting, efectos, accesibilidad y arquitectura
Arquitectura y datosTodo en la ActivitySeparo UI y datosUso Room, Retrofit, Hilt y repositoriosJustifico fuentes de verdad, transacciones, errores y límites
ComunicaciónNo sé explicarloLo explico con muletillasLo explico en 2 minutosLo explico en inglés, con decisiones y límites

Nivel objetivo realista

Para un puesto Android mid fuerte:

  • Ningún 0.

  • Al menos 2 en todas las dimensiones.

  • 3 en al menos cuatro dimensiones.

  • Una dimensión con 3 debe tener evidencia: código, test o grabación.

Para senior no basta con esta rúbrica. Senior implica años de producción, incidentes, diseño de sistemas, mentoría y decisiones con coste. Esta ruta te lleva a mid fuerte, no a senior.

Preguntas de autoevaluación

Este es el único banco de preguntas del laboratorio. Responde sin mirar y en menos de un minuto. Si necesitas mirar, marca la pregunta para repasar.

  • ⭐ = imprescindible para la entrevista. Si vas justo de tiempo, repasa solo esas.

  • → §x = sección de docs/ANDROID_FLUENCY.md con la explicación completa.

  • Cada ⭐ trae repreguntas: lo que un senior te preguntaría justo después. Practícalas en voz alta; es donde se nota si lo entiendes o lo has memorizado.

  • Las preguntas abiertas de diseño ("¿cómo diseñarías…?") están en docs/MOBILE_DESIGN.md.

Kotlin

  1. ¿val implica inmutabilidad profunda?

    No. val impide reasignar la referencia; el objeto puede ser mutable (val list = mutableListOf(1)).

  2. ¿Qué genera un data class?

    equals, hashCode, toString, copy y componentN. No genera un constructor sin parámetros ni copyWith.

  3. ¿Cuándo usar sealed interface en vez de enum?

    Cuando cada caso lleva datos distintos o cuando quieres exhaustividad con payloads. Un enum es para un conjunto fijo de valores sin datos propios.

  4. ¿Diferencia entre object y companion object?

    object es un singleton de nivel superior; companion object es un singleton asociado a una clase, útil para factories o constantes. → §3.4

  5. ⭐ ¿lateinit o by lazy?

    lateinit para var no nulo que se inicializa después (inyección de campos, tests); by lazy para val que se calcula la primera vez y es thread-safe por defecto. lateinit no vale para tipos primitivos. → §3.4

    Repreguntas:

    • ¿Qué pasa si lees un lateinit antes de inicializarlo? Lanza UninitializedPropertyAccessException. Existe ::prop.isInitialized, pero si lo necesitas a menudo, la propiedad debería ser nullable o lazy.

    • ¿by lazy tiene coste? Por defecto es SYNCHRONIZED: usa un lock. Si solo se accede desde un hilo, lazy(LazyThreadSafetyMode.NONE) lo evita.

    • ¿Dónde has visto lateinit en Android? En inyección por campo con Hilt (@Inject lateinit var analytics: Analytics en una Activity) y en tests que inicializan en @Before.

  6. ⭐ ¿value class o typealias?

    value class crea un tipo distinto en compilación sin coste en runtime: no puedes pasar un AccountId donde va un UserId. typealias solo es otro nombre: Cents y Long son intercambiables. → §3.3

    Repreguntas:

    • ¿Una value class nunca se boxea? Sí se boxea cuando se usa como genérico, nullable o a través de una interfaz (por ejemplo, en un List<AccountId>). Solo es gratis en el uso directo.

    • ¿Qué restricciones tiene? Una sola propiedad val en el constructor, no puede heredar de clases (sí implementar interfaces) y en la JVM necesita @JvmInline. Las funciones que la reciben llevan el nombre alterado para Java.

    • ¿Cómo la serializas? kotlinx.serialization la serializa como su valor interno si la marcas @Serializable. Con Room o con convertidores de otras librerías suele hacer falta un converter.

  7. ¿== vs ===?

    == llama a equals; === compara identidad de referencia (el identical() de Dart).

  8. ¿apply vs also?

    Ambos devuelven el receptor. apply usa this y sirve para configurar; also usa it y sirve para efectos laterales. → §3.1

  9. ¿Qué hace inline con reified?

    inline copia el cuerpo en el call site; reified permite usar T::class o is T en runtime: al inlinear, el compilador sustituye T por el tipo concreto y esquiva el borrado de tipos de la JVM.

  10. ¿Qué significa out y in?

    out T es covarianza: un Producer<String> se puede usar como Producer<Any>; in T es contravarianza: un Consumer<Any> se puede usar como Consumer<String>.

  11. ¿Unit vs Nothing?

    Unit es “no devuelve nada útil”, como void. Nothing es “no devuelve nunca”, porque lanza o entra en bucle infinito.

  12. ¿Sequence vs List?

    List es eager y en memoria; Sequence es lazy y evalúa por demanda. Para cadenas largas con take, Sequence evita trabajo.

  13. ¿Result vs excepción?

    Result modela errores esperados como datos y obliga a tratarlos. Las excepciones son para fallos excepcionales o de programación. No uses Result para todo.

  14. ¿copy() es profundo?

    No. Copia las referencias de las propiedades. Si una propiedad es una lista mutable, ambos objetos comparten la lista.

  15. ¿const val vs val?

    const val es una constante de compilación, solo para tipos primitivos y String en object/top-level. val puede calcularse en runtime.

  16. ¿Las clases son open por defecto?

    No. En Kotlin son final por defecto. Hay que marcar open para heredar y override para sobrescribir.

  17. ⭐ ¿Cómo haces que una función Kotlin sea cómoda desde Java?

    @JvmStatic para miembros de companion, @JvmOverloads para generar sobrecargas de los parámetros por defecto, @JvmField para exponer campos y @Throws para declarar excepciones comprobadas. Y anotar la nulabilidad en el borde, porque desde Java llegan tipos plataforma (String!). → §3.7

    Repreguntas:

    • ¿Qué es un tipo plataforma y por qué es peligroso? String!: Kotlin no sabe si puede ser null y no te obliga a comprobarlo. Si llega null y lo tratas como String, tienes un NPE en runtime. Se arregla anotando en Java (@Nullable/@NonNull) o declarando el tipo en Kotlin.

    • ¿Cómo llamas a una extensión desde Java? Como método estático de la clase del archivo: MoneyKt.formatCents(value). @file:JvmName("Money") cambia ese nombre.

    • ¿Y a un object? Singleton.INSTANCE.metodo(), o Singleton.metodo() si el método lleva @JvmStatic.

Coroutines y Flow

  1. ¿Cómo funciona suspend?

    El compilador transforma la función en una máquina de estados con continuaciones. No bloquea el hilo; suspende y reanuda.

  2. ⭐ ¿Qué es structured concurrency y por qué evitar GlobalScope?

    Toda coroutine vive en un scope con un ciclo de vida (viewModelScope, lifecycleScope). Si el scope se cancela, se cancelan sus hijas, y un fallo se propaga al padre. GlobalScope no tiene dueño: nada la cancela, así que provoca fugas y trabajo huérfano. → §4

    Repreguntas:

    • ¿Qué scope usarías para un trabajo que debe sobrevivir a salir de la pantalla, como enviar una transferencia? viewModelScope no, porque se cancela. Para algo corto, un applicationScope inyectado (CoroutineScope(SupervisorJob() + dispatcher)). Si debe completarse aunque se mate el proceso, WorkManager.

    • ¿Qué pasa si falla una coroutine dentro de viewModelScope? viewModelScope usa SupervisorJob, así que no cancela a las hermanas. Pero si nadie captura la excepción, llega al handler de excepciones no capturadas y la app crashea. Captúrala dentro o usa un CoroutineExceptionHandler.

    • ¿Job vs SupervisorJob? Con Job, el fallo de una hija cancela al padre y a las hermanas. Con SupervisorJob, el fallo se queda en esa hija.

  3. ¿launch vs async?

    launch devuelve un Job y no propaga resultado; async devuelve Deferred<T> y se usa con await(). Un fallo en async cancela el scope padre aunque nunca hagas await (salvo en supervisorScope); el await además relanza la excepción.

  4. ¿coroutineScope vs supervisorScope?

    coroutineScope cancela todos los hijos si uno falla. supervisorScope no cancela a los hermanos; cada hijo maneja su error.

  5. ¿Cómo se cancela una coroutine?

    Se cancela el Job/scope; la coroutine debe ser cooperativa: usar suspend cancelables, comprobar isActive o ensureActive(). No captures CancellationException sin relanzarla.

  6. ⭐ ¿Qué problema tiene runCatching dentro de una coroutine?

    Atrapa todo Throwable, incluida CancellationException. La cancelación se convierte en un Result.failure y el código de después sigue ejecutándose. Solución: un try/catch que relance CancellationException, o un helper tipo runSuspendCatching (nivel 7 del lab). → §3.6

    Repreguntas:

    • ¿Y si capturas Exception en lugar de Throwable? Tampoco basta: CancellationException hereda de IllegalStateException, así que es una Exception. Hay que relanzarla de forma explícita.

    • ¿Y si llamas a ensureActive() después del runCatching? Lo arregla a medias: detiene la coroutine en ese punto, pero es frágil y fácil de olvidar. Mejor no tragarse la excepción.

    • ¿Y withTimeout? Lanza TimeoutCancellationException, que es una CancellationException. Si quieres tratar el timeout como error de negocio, usa withTimeoutOrNull o captúrala específicamente.

  7. ¿Dispatchers.IO vs Default?

    IO para bloqueo de I/O (disco, red); Default para CPU. No uses IO para todo. Las funciones suspend deben ser main-safe: el que hace I/O cambia de dispatcher, no el que llama. → §1.5

  8. ¿Flow frío vs caliente?

    Un Flow frío no emite hasta que se colecta y cada colector ejecuta el bloque. StateFlow/SharedFlow son calientes y comparten emisiones.

  9. ⭐ ¿StateFlow, SharedFlow o Channel para eventos?

    StateFlow siempre tiene un valor y conflaciona: es estado. SharedFlow puede no tener valor y se configura con replay: eventos para varios colectores, pero sin colector se pierden. Channel + receiveAsFlow() entrega cada evento una vez a un único colector y lo guarda hasta que alguien lo recoge. Google recomienda además modelar eventos como estado que la UI “consume”. → §4

    Repreguntas:

    • ¿Por qué StateFlow no sirve para eventos? Porque conflaciona y compara con equals: si emites el mismo valor no notifica, y si emites rápido un colector lento solo ve el último. Un evento puede perderse o no dispararse dos veces.

    • ¿Se pierde un evento de Channel durante una rotación? No: se queda en el buffer hasta que la nueva Activity empieza a colectar. Con un SharedFlow sin replay sí se perdería, porque nadie estaba escuchando.

    • ¿Qué recomienda Google y por qué? Modelar el evento como estado (por ejemplo, receipt: Receipt? en el UiState) y que la UI llame a onReceiptShown() para limpiarlo. Es más verboso, pero es fácil de testear y puede sobrevivir a process death.

  10. ⭐ ¿Qué hace stateIn y por qué WhileSubscribed(5_000)?

    Convierte un Flow en StateFlow con un scope, una estrategia de inicio y un valor inicial. Con WhileSubscribed(5_000) el upstream sigue vivo 5 s tras el último colector: una rotación (que tarda menos) no reinicia la carga, pero si la app pasa a background el trabajo se para. → §4

    Repreguntas:

    • ¿Por qué no SharingStarted.Eagerly? Mantiene el upstream activo mientras viva el ViewModel, aunque la app esté en background: consultas a la base de datos, sockets y batería para nada.

    • ¿Y Lazily? Empieza con el primer colector y ya no para nunca. Mismo problema en background.

    • ¿Qué ve el usuario al volver tras más de 5 s en background? El upstream se reinicia, pero el StateFlow conserva el último valor mientras recarga, así que la pantalla no parpadea a vacío.

  11. ¿flatMapLatest vs flatMapMerge?

    flatMapLatest cancela el flujo anterior cuando llega uno nuevo; ideal para búsquedas. flatMapMerge ejecuta varios en paralelo y mezcla emisiones.

  12. ¿Qué hace runTest?

    Ejecuta un test de coroutines con un scheduler virtual; delay no espera tiempo real. Permite advanceUntilIdle() y tests deterministas.

  13. ¿Para qué sirve Turbine?

    Para testear Flow con una API de awaitItem, awaitComplete y cancelAndIgnoreRemainingEvents, sin recolectar manualmente.

Plataforma Android

  1. ⭐ ¿Qué pasa con tu estado al rotar la pantalla? ¿Y con process death?

    Al rotar, la Activity se destruye y recrea: el ViewModel sobrevive y remember no. En process death el sistema mata el proceso en background y al volver recrea la pantalla: el ViewModel también muere; solo sobrevive rememberSaveable, SavedStateHandle, el back stack y lo que esté en disco (Room, DataStore). → §1.4

    Repreguntas:

    • ¿Cómo lo pruebas? Con "Don't keep activities" en opciones de desarrollador o con adb shell am kill <package> con la app en background. En tests: StateRestorationTester en Compose, y crear el ViewModel con un SavedStateHandle precargado.

    • ¿Qué guardarías en SavedStateHandle y qué no? IDs, lo que ha escrito el usuario, filtros: cosas pequeñas y serializables. Las listas de datos no: el Bundle tiene límite (te arriesgas a TransactionTooLargeException) y se recargan de Room o de red.

    • ¿Y si declaro configChanges en el manifest para que no se recree la Activity? Evita la recreación al rotar, pero no evita process death, y te obliga a gestionar los cambios a mano. El estado tiene que estar bien guardado igualmente.

  2. ⭐ ¿Por qué no guardar un Context de Activity en un ViewModel?

    El ViewModel vive más que la Activity: al rotar, retendría la Activity destruida (memory leak). Si necesitas recursos, mapea a un UiText o usa applicationContext en la capa de datos. → §1.2

    Repreguntas:

    • ¿Y AndroidViewModel? Recibe la Application, no la Activity, así que no filtra memoria. Pero acopla el ViewModel a Android y complica los tests: mejor inyectar lo que necesites.

    • ¿Cómo detectas un leak? Con LeakCanary en debug: te avisa y muestra la cadena de referencias que retiene la Activity.

    • ¿Otros leaks típicos? Listeners o callbacks sin desregistrar (en Compose, DisposableEffect), coroutines en GlobalScope que capturan la Activity, y lambdas o clases internas que capturan this dentro de un singleton.

  3. ⭐ ¿Qué es un ANR y cómo lo evitas?

    Application Not Responding: el main thread bloqueado unos 5 s ante un input. Se evita sacando disco, red y CPU pesada del main thread con withContext(Dispatchers.IO/Default), y detectándolo con StrictMode y Android Vitals. → §1.5

    Repreguntas:

    • ¿Una función suspend nunca bloquea el main thread? Falso. suspend no cambia de hilo por sí sola: si dentro haces I/O bloqueante sin withContext, bloquea Main igual.

    • ¿Cómo investigas un ANR en producción? En Android Vitals (Play Console) o Crashlytics ves el stack del main thread en el momento del ANR: I/O, un lock, una llamada binder lenta…

    • ¿Y el jank en Compose? Cálculos pesados durante la composición (ordenar una lista enorme en cada recomposición). Muévelos al ViewModel o a remember con keys.

Compose

  1. ¿Qué es recomposición?

    Compose vuelve a ejecutar las funciones @Composable cuyas entradas cambiaron. No reconstruye un árbol de widgets como Flutter; actualiza las partes necesarias.

  2. ⭐ ¿Qué hace que un composable se salte la recomposición?

    Que sus parámetros sean estables y no hayan cambiado. List<T> es inestable para el compilador; se arregla con ImmutableList, @Immutable/@Stable, o con strong skipping (activo por defecto desde Kotlin 2.0.20). Se comprueba con el Layout Inspector y los compiler reports. → §5

    Repreguntas:

    • ¿Qué hace que un tipo sea estable? Que sea inmutable o que avise a Compose de sus cambios (State), y que todas sus propiedades públicas sean estables. Primitivos, String y lambdas lo son.

    • Con strong skipping, ¿ya no importa la estabilidad? Importa menos: los parámetros inestables se comparan por referencia (===). Si en cada recomposición creas una lista nueva con el mismo contenido, sigue recomponiendo.

    • ¿Cómo lo diagnosticas? Con el Layout Inspector (contador de recomposiciones y skips) y los compiler reports de Compose, que dicen qué clases y funciones son estables y saltables.

  3. ¿remember vs rememberSaveable?

    remember sobrevive a recomposiciones; rememberSaveable sobrevive además a recreación y a muerte de proceso si el valor es guardable.

  4. ¿Qué es state hoisting?

    Mover el estado hacia arriba y pasar value + onValueChange hacia abajo. La UI queda tonta y testeable; el dueño del estado decide.

  5. ¿LaunchedEffect vs DisposableEffect?

    LaunchedEffect lanza una coroutine ligada a la composición y la cancela al salir. DisposableEffect registra un cleanup para recursos no suspendidos.

  6. ⭐ ¿LaunchedEffect vs rememberCoroutineScope?

    LaunchedEffect(key) lanza una coroutine cuando el composable entra o cambia la key: efectos dirigidos por estado. rememberCoroutineScope() te da un scope para lanzar coroutines desde callbacks de usuario (un onClick que abre un bottom sheet). → §5

    Repreguntas:

    • ¿Qué pasa con LaunchedEffect(Unit)? Se ejecuta una vez al entrar en composición y no se relanza. Si depende de un valor que cambia, ese valor tiene que ser la key. Al rotar sí vuelve a ejecutarse, porque la composición se recrea.

    • ¿Por qué no lanzar la coroutine directamente en el cuerpo del composable? Porque el cuerpo se ejecuta en cada recomposición: lanzarías una coroutine nueva cada vez. Es un side effect sin control.

    • ¿Y si el callback que usas dentro del LaunchedEffect cambia? rememberUpdatedState(callback) te da siempre la última versión sin reiniciar el efecto.

  7. ¿Por qué collectAsStateWithLifecycle?

    Porque respeta el ciclo de vida: deja de colectar cuando la UI no está visible y evita trabajo y fugas.

  8. ¿remember o ViewModel para estado de pantalla?

    remember para estado efímero de UI; ViewModel para estado que sobrevive a configuración, navegación y necesita lógica/test.

Arquitectura, datos y tests

  1. ⭐ ¿Cuándo crearías un UseCase y cuándo no?

    Cuando la lógica se reutiliza en varios ViewModels o combina varios repositorios. Si solo reenvía una llamada al repositorio, sobra: la guía oficial lo considera opcional. → §6

    Repreguntas:

    • ¿Dónde vive un UseCase y de qué depende? En la capa de dominio y depende de interfaces de repositorio, sin Android. Así se testea en JVM y se podría compartir con KMP.

    • ¿Por qué operator fun invoke? Para llamarlo como una función: submitTransfer(request). Es convención, no obligación.

    • ¿Uno por pantalla? No: uno por operación de negocio. Si tienes UseCases que solo reenvían al repositorio, son ruido.

  2. ⭐ ¿Cómo diseñarías una pantalla offline-first? ¿Por qué Room es la fuente de verdad?

    La UI observa Room; la red solo actualiza Room. Así hay una sola verdad, funciona offline y las consultas son reactivas. El repositorio decide cuándo refrescar y cómo propagar errores de red sin vaciar la pantalla. → §6

    Repreguntas:

    • ¿Cómo resuelves conflictos entre lo local y el servidor? En banca el servidor es autoritativo: lo local es caché. Las escrituras (una transferencia) no se aplican como definitivas: se marcan como pendientes, con idempotency key, hasta que el servidor confirma.

    • ¿Cuándo refrescas? Al abrir la pantalla (muestras la caché y revalidas), con pull to refresh y, si hace falta, periódicamente con WorkManager. Enseña la hora de la última actualización.

    • ¿Qué muestras si falla la red y tienes caché? Los datos en caché y un aviso no bloqueante (banner o snackbar). Una pantalla de error completa solo si no hay nada que mostrar.

  3. ⭐ ¿Qué aporta Hilt frente a construir dependencias a mano?

    El grafo se valida en compilación, gestiona scopes y ciclo de vida (@Singleton, @ViewModelScoped), se integra con ViewModel y navegación, y en tests permite sustituir módulos. A mano funciona en apps pequeñas, pero escala mal. → §6

    Repreguntas:

    • ¿@Binds o @Provides? @Binds enlaza una interfaz con una implementación que ya tiene @Inject constructor: es abstracto y no lleva código. @Provides cuando hay que construir algo: Retrofit, la base de datos de Room, clases de terceros.

    • ¿Cómo sustituyes dependencias en tests? En unit tests no usas Hilt: pasas fakes por constructor. En tests instrumentados, @HiltAndroidTest con @TestInstallIn o @UninstallModules.

    • ¿Hilt o Koin? Hilt (sobre Dagger) genera código y valida el grafo al compilar. Koin es más sencillo, pero resuelve en runtime: los errores aparecen al ejecutar. Koin es popular en KMP.

  4. ¿Qué scopes de Hilt existen?

    @Singleton, @ActivityRetainedScoped, @ActivityScoped, @ViewModelScoped, entre otros. El scope decide la vida de la instancia; no uses @Singleton para todo.

  5. ¿Qué es una idempotency key?

    Un identificador único por operación para que reintentar una transferencia no la ejecute dos veces. El servidor guarda la clave y devuelve el mismo resultado.

  6. ¿Cómo modelas errores esperados en una API?

    Con un tipo (Result, ApiResult, sealed) que distingue éxito, error de validación, error de red y error de servidor. No con un String suelto.

  7. ¿Qué es la pirámide de tests?

    Muchos tests unitarios rápidos, menos de integración y pocos end-to-end. En Android: dominio/ViewModel con JUnit, DAO con Room in-memory, UI con Compose Test.

  8. ⭐ ¿Fakes o mocks? ¿Por qué?

    Fakes por defecto, como recomienda Google: un FakeRepository con un MutableStateFlow interno se comporta como el real, los tests prueban comportamiento y no se rompen al refactorizar. Mocks (MockK) para verificar interacciones concretas o dependencias difíciles de falsear. → §7

    Repreguntas:

    • ¿Qué problema tienen los mocks? Acoplan el test a la implementación: verifican llamadas, así que un refactor que no cambia el comportamiento rompe tests. Y es fácil acabar testeando el mock.

    • ¿Cómo testeas un ViewModel con un fake? Fake repository con un MutableStateFlow, una MainDispatcherRule para Dispatchers.Main, emites valores en el fake y compruebas el uiState con Turbine o con .value.

    • ¿Cuándo sí usarías un mock? Para verificar una interacción que es el comportamiento en sí, como que se envió un evento de analytics, o para dependencias de terceros difíciles de falsear.

Build y release

  1. ⭐ ¿implementation vs api en Gradle?

    implementation oculta la dependencia a quien consume tu módulo: compila más rápido y no filtra detalles. api la expone; úsala solo cuando la dependencia aparece en tu API pública. → §2

    Repreguntas:

    • ¿Por qué implementation compila más rápido? Si cambia una dependencia implementation, solo se recompila tu módulo. Con api, el cambio se propaga y se recompilan también los módulos que dependen de ti.

    • ¿Qué pasa en un proyecto grande si todo es api? Recompilaciones en cascada y fugas de detalles: un módulo de feature acaba viendo Retrofit o Room y usándolos directamente.

    • ¿Cómo se comparte configuración entre módulos? Versiones en libs.versions.toml y configuración común en convention plugins dentro de build-logic.

  2. ⭐ ¿Por qué un crash solo en release?

    Casi siempre R8: ofuscó o eliminó una clase usada por reflexión (serialización, Retrofit) sin regla -keep o @Keep. Se diagnostica con el mapping.txt y reproduciendo con un build release local. → §2

    Repreguntas:

    • ¿Cómo lo reproduces en local? Instalando un build release o un build type con minify activado y firma de debug. El stack trace ofuscado se traduce con mapping.txt; Crashlytics lo sube y lo hace automáticamente.

    • ¿Qué otras causas hay, además de R8? Diferencias de configuración (URLs, flags de BuildConfig), código que solo existe en debug, logging desactivado y recursos eliminados por shrinkResources si se acceden por nombre.

    • ¿Cómo lo evitas? Tests o un smoke test en CI contra el build minificado, @Keep o reglas -keep donde haya reflexión, y preferir librerías que traen sus propias consumer rules.

  3. ⭐ ¿Cómo publicarías una release arriesgada?

    Feature flag para poder apagarla sin release, track interno o cerrado primero, después staged rollout (5 %, 20 %, 100 %) vigilando crash-free rate en Crashlytics y Android Vitals, y halt del rollout si algo se tuerce. → §2

    Repreguntas:

    • ¿Qué métricas te harían parar el rollout? Crash rate y ANR rate frente a la versión anterior (Android Vitals marca como mal comportamiento un 1,09 % de usuarios con crash y un 0,47 % con ANR), errores de negocio (transferencias fallidas) y reseñas.

    • ¿Parar el rollout deshace la actualización? No. Quien ya actualizó se queda con esa versión. Por eso el feature flag es la palanca rápida; si no, toca publicar otra versión con un versionCode mayor.

    • ¿Qué haces antes de empezar el rollout? Track interno y QA, pruebas en dispositivos representativos (Firebase Test Lab) y revisar a fondo las migraciones de base de datos, que no tienen vuelta atrás.

La pregunta Flutter

  1. ⭐ ¿Qué ventaja tiene KMP frente a Flutter y cuándo elegirías cada uno?

    "Flutter comparte UI y lógica con un único motor de render; KMP comparte dominio y datos y deja la UI nativa (o Compose Multiplatform). En un banco, con equipos nativos fuertes y requisitos de plataforma (biometría, accesibilidad, seguridad), KMP encaja mejor; en un producto que necesita velocidad con un equipo pequeño, Flutter. Vengo de Flutter y por eso valoro lo que da el nativo: acceso directo a la plataforma sin channels y herramientas de primera." Te la van a hacer casi seguro. → §8

    Repreguntas:

    • ¿Qué compartirías con KMP en una app de banco? Dominio, validaciones, networking (Ktor) y persistencia (SQLDelight o Room KMP); opcionalmente la lógica de presentación. La UI, nativa.

    • ¿Por qué quieres pasarte a nativo? "Quiero estar más cerca de la plataforma: ciclo de vida, rendimiento, accesibilidad y seguridad. En Flutter acababa usando channels para eso; en nativo es de primera."

    • ¿Qué te llevas de Flutter que te sirva aquí? Pensar en estado inmutable y flujo unidireccional (Bloc ≈ MVI) y en componer UI declarativa: Compose usa el mismo modelo mental.

Ejercicios de lectura de código

Di el resultado sin ejecutar.

1

val result = listOf(1, 2, 3)
    .map { it * 2 }
    .filter { it > 2 }
println(result)

Respuesta: [4, 6].

2

val name: String? = null
println(name?.length ?: -1)

Respuesta: -1.

3

sealed interface State {
    data object Loading : State
    data class Ready(val value: String) : State
}

fun render(state: State): String = when (state) {
    State.Loading -> "loading"
}

Respuesta: no compila. El when no es exhaustivo; falta State.Ready.

4

val result = sequence {
    yield(1)
    yield(2)
    yield(3)
}.map {
    println("map $it")
    it * 10
}.take(1).toList()

println(result)

Respuesta: imprime map 1 y [10]. La secuencia es lazy y take(1) corta después del primer elemento.

5

runTest {
    delay(10_000)
    println("done")
}

Respuesta: imprime done casi inmediatamente. runTest usa tiempo virtual.

Simulacro de entrevista de 45 minutos

MinutoBloqueQué evalúas
0–5PresentaciónClaridad, motivación, inglés
5–20Leer y refactorizar un archivo de KontorNombres, tipos, tests, límites
20–30Coroutines y FlowCancelación, estado, errores
30–40Compose y arquitectura, o un caso de MOBILE_DESIGN.mdState hoisting, ViewModel, Room; preguntas antes de diseñar, errores y trade-offs
40–45Tus preguntasCuriosidad y criterio

Hazlo en voz alta y grábalo. Si no puedes explicar una decisión en 90 segundos, vuelve a estudiarla.

Scorecard mensual

Copia esta tabla en notas/scorecard-mes-N.md:

DimensiónNivel (0–3)EvidenciaSiguiente acción
Sintaxis e idioms
Tipos y nulabilidad
Dominio
Colecciones
Genéricos
Coroutines y Flow
Tests y debugging
Android/Compose
Arquitectura y datos
Comunicación

La pregunta incómoda

Si suspendes una dimensión, no significa que no valgas. Significa que todavía no tienes evidencia. La solución no es leer más: es escribir un test, una pantalla o una explicación que produzca esa evidencia.

Y si todo te sale bien, súbete la dificultad: un proyecto con requisitos ambiguos, un bug de concurrencia, un code review de otra persona o una contribución open source.

Fuente: docs/ASSESSMENT.md