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
Lee cada dimensión.
Asigna un nivel 0–3 con una prueba concreta.
Escribe la evidencia: un test, un archivo, una explicación grabada.
Elige una dimensión para mejorar este mes.
Vuelve a puntuar en cuatro semanas.
Rúbrica
| Dimensión | 0 — No | 1 — Reconozco | 2 — Lo hago | 3 — Lo explico y decido |
|---|---|---|---|---|
| Sintaxis e idioms | No compilo sin copiar | Entiendo el código, pero necesito referencias | Escribo funciones y clases con documentación | Escribo Kotlin idiomático y sé cuándo no usar un idiom |
| Tipos y nulabilidad | Uso Any y !! | Entiendo String?, pero peleo con el compilador | Modelo con tipos y evito !! | Diseño APIs que hacen imposible el estado inválido |
| Dominio | Todo son String y Boolean | Separo clases, pero mezclo capas | Modelo Money, Iban, Transfer con tests | Decido qué es entidad, value object, error y caso de uso |
| Colecciones | Uso bucles para todo | Conozco map/filter, pero no los combino | Transformo datos con groupBy, sumOf, associateBy | Elijo la estructura por coste y legibilidad |
| Genéricos | Evito leerlos | Entiendo List<T> | Escribo Repository<T, ID> y ApiResult<T> | Entiendo variance y sé cuándo una abstracción sobra |
| Coroutines y Flow | Uso GlobalScope o Thread.sleep | Entiendo suspend, pero no la cancelación | Uso scopes, runTest, StateFlow y Turbine | Diseño concurrencia, cancelación, reintentos y backpressure |
| Tests y debugging | Copio tests | Escribo tests del camino feliz | Cubro bordes y fallos, aíslo bugs | Diseño la estrategia de tests y sé qué no testear |
| Android/Compose | No sé por dónde empezar | Entiendo un @Composable | Hago pantallas con estados, navegación y ViewModel | Decido state hoisting, efectos, accesibilidad y arquitectura |
| Arquitectura y datos | Todo en la Activity | Separo UI y datos | Uso Room, Retrofit, Hilt y repositorios | Justifico fuentes de verdad, transacciones, errores y límites |
| Comunicación | No sé explicarlo | Lo explico con muletillas | Lo explico en 2 minutos | Lo 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 dedocs/ANDROID_FLUENCY.mdcon 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
¿
valimplica inmutabilidad profunda?No.
valimpide reasignar la referencia; el objeto puede ser mutable (val list = mutableListOf(1)).¿Qué genera un
data class?equals,hashCode,toString,copyycomponentN. No genera un constructor sin parámetros nicopyWith.¿Cuándo usar
sealed interfaceen vez deenum?Cuando cada caso lleva datos distintos o cuando quieres exhaustividad con payloads. Un
enumes para un conjunto fijo de valores sin datos propios.¿Diferencia entre
objectycompanion object?objectes un singleton de nivel superior;companion objectes un singleton asociado a una clase, útil para factories o constantes. → §3.4⭐ ¿
lateinitoby lazy?lateinitparavarno nulo que se inicializa después (inyección de campos, tests);by lazyparavalque se calcula la primera vez y es thread-safe por defecto.lateinitno vale para tipos primitivos. → §3.4Repreguntas:
¿Qué pasa si lees un
lateinitantes de inicializarlo? LanzaUninitializedPropertyAccessException. Existe::prop.isInitialized, pero si lo necesitas a menudo, la propiedad debería ser nullable olazy.¿
by lazytiene coste? Por defecto esSYNCHRONIZED: usa un lock. Si solo se accede desde un hilo,lazy(LazyThreadSafetyMode.NONE)lo evita.¿Dónde has visto
lateiniten Android? En inyección por campo con Hilt (@Inject lateinit var analytics: Analyticsen una Activity) y en tests que inicializan en@Before.
⭐ ¿
value classotypealias?value classcrea un tipo distinto en compilación sin coste en runtime: no puedes pasar unAccountIddonde va unUserId.typealiassolo es otro nombre:CentsyLongson intercambiables. → §3.3Repreguntas:
¿Una
value classnunca se boxea? Sí se boxea cuando se usa como genérico, nullable o a través de una interfaz (por ejemplo, en unList<AccountId>). Solo es gratis en el uso directo.¿Qué restricciones tiene? Una sola propiedad
valen 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.
¿
==vs===?==llama aequals;===compara identidad de referencia (elidentical()de Dart).¿
applyvsalso?Ambos devuelven el receptor.
applyusathisy sirve para configurar;alsousaity sirve para efectos laterales. → §3.1¿Qué hace
inlineconreified?inlinecopia el cuerpo en el call site;reifiedpermite usarT::classois Ten runtime: al inlinear, el compilador sustituyeTpor el tipo concreto y esquiva el borrado de tipos de la JVM.¿Qué significa
outyin?out Tes covarianza: unProducer<String>se puede usar comoProducer<Any>;in Tes contravarianza: unConsumer<Any>se puede usar comoConsumer<String>.¿
UnitvsNothing?Unites “no devuelve nada útil”, comovoid.Nothinges “no devuelve nunca”, porque lanza o entra en bucle infinito.¿
SequencevsList?Listes eager y en memoria;Sequencees lazy y evalúa por demanda. Para cadenas largas contake,Sequenceevita trabajo.¿
Resultvs excepción?Resultmodela errores esperados como datos y obliga a tratarlos. Las excepciones son para fallos excepcionales o de programación. No usesResultpara todo.¿
copy()es profundo?No. Copia las referencias de las propiedades. Si una propiedad es una lista mutable, ambos objetos comparten la lista.
¿
const valvsval?const vales una constante de compilación, solo para tipos primitivos yStringenobject/top-level.valpuede calcularse en runtime.¿Las clases son
openpor defecto?No. En Kotlin son
finalpor defecto. Hay que marcaropenpara heredar yoverridepara sobrescribir.⭐ ¿Cómo haces que una función Kotlin sea cómoda desde Java?
@JvmStaticpara miembros decompanion,@JvmOverloadspara generar sobrecargas de los parámetros por defecto,@JvmFieldpara exponer campos y@Throwspara declarar excepciones comprobadas. Y anotar la nulabilidad en el borde, porque desde Java llegan tipos plataforma (String!). → §3.7Repreguntas:
¿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 comoString, 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(), oSingleton.metodo()si el método lleva@JvmStatic.
Coroutines y Flow
¿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.
⭐ ¿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.GlobalScopeno tiene dueño: nada la cancela, así que provoca fugas y trabajo huérfano. → §4Repreguntas:
¿Qué scope usarías para un trabajo que debe sobrevivir a salir de la pantalla, como enviar una transferencia?
viewModelScopeno, porque se cancela. Para algo corto, unapplicationScopeinyectado (CoroutineScope(SupervisorJob() + dispatcher)). Si debe completarse aunque se mate el proceso, WorkManager.¿Qué pasa si falla una coroutine dentro de
viewModelScope?viewModelScopeusaSupervisorJob, 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 unCoroutineExceptionHandler.¿
JobvsSupervisorJob? ConJob, el fallo de una hija cancela al padre y a las hermanas. ConSupervisorJob, el fallo se queda en esa hija.
¿
launchvsasync?launchdevuelve unJoby no propaga resultado;asyncdevuelveDeferred<T>y se usa conawait(). Un fallo enasynccancela el scope padre aunque nunca hagasawait(salvo ensupervisorScope); elawaitademás relanza la excepción.¿
coroutineScopevssupervisorScope?coroutineScopecancela todos los hijos si uno falla.supervisorScopeno cancela a los hermanos; cada hijo maneja su error.¿Cómo se cancela una coroutine?
Se cancela el
Job/scope; la coroutine debe ser cooperativa: usarsuspendcancelables, comprobarisActiveoensureActive(). No capturesCancellationExceptionsin relanzarla.⭐ ¿Qué problema tiene
runCatchingdentro de una coroutine?Atrapa todo
Throwable, incluidaCancellationException. La cancelación se convierte en unResult.failurey el código de después sigue ejecutándose. Solución: untry/catchque relanceCancellationException, o un helper tiporunSuspendCatching(nivel 7 del lab). → §3.6Repreguntas:
¿Y si capturas
Exceptionen lugar deThrowable? Tampoco basta:CancellationExceptionhereda deIllegalStateException, así que es unaException. Hay que relanzarla de forma explícita.¿Y si llamas a
ensureActive()después delrunCatching? 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? LanzaTimeoutCancellationException, que es unaCancellationException. Si quieres tratar el timeout como error de negocio, usawithTimeoutOrNullo captúrala específicamente.
¿
Dispatchers.IOvsDefault?IOpara bloqueo de I/O (disco, red);Defaultpara CPU. No usesIOpara todo. Las funcionessuspenddeben ser main-safe: el que hace I/O cambia de dispatcher, no el que llama. → §1.5¿
Flowfrío vs caliente?Un
Flowfrío no emite hasta que se colecta y cada colector ejecuta el bloque.StateFlow/SharedFlowson calientes y comparten emisiones.⭐ ¿
StateFlow,SharedFlowoChannelpara eventos?StateFlowsiempre tiene un valor y conflaciona: es estado.SharedFlowpuede no tener valor y se configura conreplay: 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”. → §4Repreguntas:
¿Por qué
StateFlowno sirve para eventos? Porque conflaciona y compara conequals: 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
Channeldurante una rotación? No: se queda en el buffer hasta que la nueva Activity empieza a colectar. Con unSharedFlowsinreplaysí se perdería, porque nadie estaba escuchando.¿Qué recomienda Google y por qué? Modelar el evento como estado (por ejemplo,
receipt: Receipt?en elUiState) y que la UI llame aonReceiptShown()para limpiarlo. Es más verboso, pero es fácil de testear y puede sobrevivir a process death.
⭐ ¿Qué hace
stateIny por quéWhileSubscribed(5_000)?Convierte un
FlowenStateFlowcon un scope, una estrategia de inicio y un valor inicial. ConWhileSubscribed(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. → §4Repreguntas:
¿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
StateFlowconserva el último valor mientras recarga, así que la pantalla no parpadea a vacío.
¿
flatMapLatestvsflatMapMerge?flatMapLatestcancela el flujo anterior cuando llega uno nuevo; ideal para búsquedas.flatMapMergeejecuta varios en paralelo y mezcla emisiones.¿Qué hace
runTest?Ejecuta un test de coroutines con un scheduler virtual;
delayno espera tiempo real. PermiteadvanceUntilIdle()y tests deterministas.¿Para qué sirve Turbine?
Para testear
Flowcon una API deawaitItem,awaitCompleteycancelAndIgnoreRemainingEvents, sin recolectar manualmente.
Plataforma Android
⭐ ¿Qué pasa con tu estado al rotar la pantalla? ¿Y con process death?
Al rotar, la
Activityse destruye y recrea: elViewModelsobrevive yrememberno. En process death el sistema mata el proceso en background y al volver recrea la pantalla: elViewModeltambién muere; solo sobreviverememberSaveable,SavedStateHandle, el back stack y lo que esté en disco (Room, DataStore). → §1.4Repreguntas:
¿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:StateRestorationTesteren Compose, y crear el ViewModel con unSavedStateHandleprecargado.¿Qué guardarías en
SavedStateHandley 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 aTransactionTooLargeException) y se recargan de Room o de red.¿Y si declaro
configChangesen 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.
⭐ ¿Por qué no guardar un
Contextde 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
UiTexto usaapplicationContexten la capa de datos. → §1.2Repreguntas:
¿Y
AndroidViewModel? Recibe laApplication, 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 enGlobalScopeque capturan la Activity, y lambdas o clases internas que capturanthisdentro de un singleton.
⭐ ¿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.5Repreguntas:
¿Una función
suspendnunca bloquea el main thread? Falso.suspendno cambia de hilo por sí sola: si dentro haces I/O bloqueante sinwithContext, bloqueaMainigual.¿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
remembercon keys.
Compose
¿Qué es recomposición?
Compose vuelve a ejecutar las funciones
@Composablecuyas entradas cambiaron. No reconstruye un árbol de widgets como Flutter; actualiza las partes necesarias.⭐ ¿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 conImmutableList,@Immutable/@Stable, o con strong skipping (activo por defecto desde Kotlin 2.0.20). Se comprueba con el Layout Inspector y los compiler reports. → §5Repreguntas:
¿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,Stringy 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.
¿
remembervsrememberSaveable?remembersobrevive a recomposiciones;rememberSaveablesobrevive además a recreación y a muerte de proceso si el valor es guardable.¿Qué es state hoisting?
Mover el estado hacia arriba y pasar
value+onValueChangehacia abajo. La UI queda tonta y testeable; el dueño del estado decide.¿
LaunchedEffectvsDisposableEffect?LaunchedEffectlanza una coroutine ligada a la composición y la cancela al salir.DisposableEffectregistra un cleanup para recursos no suspendidos.⭐ ¿
LaunchedEffectvsrememberCoroutineScope?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 (unonClickque abre un bottom sheet). → §5Repreguntas:
¿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
LaunchedEffectcambia?rememberUpdatedState(callback)te da siempre la última versión sin reiniciar el efecto.
¿Por qué
collectAsStateWithLifecycle?Porque respeta el ciclo de vida: deja de colectar cuando la UI no está visible y evita trabajo y fugas.
¿
rememberoViewModelpara estado de pantalla?rememberpara estado efímero de UI;ViewModelpara estado que sobrevive a configuración, navegación y necesita lógica/test.
Arquitectura, datos y tests
⭐ ¿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.
⭐ ¿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.
⭐ ¿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. → §6Repreguntas:
¿
@Bindso@Provides?@Bindsenlaza una interfaz con una implementación que ya tiene@Inject constructor: es abstracto y no lleva código.@Providescuando 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,
@HiltAndroidTestcon@TestInstallIno@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.
¿Qué scopes de Hilt existen?
@Singleton,@ActivityRetainedScoped,@ActivityScoped,@ViewModelScoped, entre otros. El scope decide la vida de la instancia; no uses@Singletonpara todo.¿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.
¿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 unStringsuelto.¿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.
⭐ ¿Fakes o mocks? ¿Por qué?
Fakes por defecto, como recomienda Google: un
FakeRepositorycon unMutableStateFlowinterno 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. → §7Repreguntas:
¿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, unaMainDispatcherRuleparaDispatchers.Main, emites valores en el fake y compruebas eluiStatecon 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
⭐ ¿
implementationvsapien Gradle?implementationoculta la dependencia a quien consume tu módulo: compila más rápido y no filtra detalles.apila expone; úsala solo cuando la dependencia aparece en tu API pública. → §2Repreguntas:
¿Por qué
implementationcompila más rápido? Si cambia una dependenciaimplementation, solo se recompila tu módulo. Conapi, 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.tomly configuración común en convention plugins dentro debuild-logic.
⭐ ¿Por qué un crash solo en release?
Casi siempre R8: ofuscó o eliminó una clase usada por reflexión (serialización, Retrofit) sin regla
-keepo@Keep. Se diagnostica con elmapping.txty reproduciendo con un build release local. → §2Repreguntas:
¿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 porshrinkResourcessi se acceden por nombre.¿Cómo lo evitas? Tests o un smoke test en CI contra el build minificado,
@Keepo reglas-keepdonde haya reflexión, y preferir librerías que traen sus propias consumer rules.
⭐ ¿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
versionCodemayor.¿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
⭐ ¿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
| Minuto | Bloque | Qué evalúas |
|---|---|---|
| 0–5 | Presentación | Claridad, motivación, inglés |
| 5–20 | Leer y refactorizar un archivo de Kontor | Nombres, tipos, tests, límites |
| 20–30 | Coroutines y Flow | Cancelación, estado, errores |
| 30–40 | Compose y arquitectura, o un caso de MOBILE_DESIGN.md | State hoisting, ViewModel, Room; preguntas antes de diseñar, errores y trade-offs |
| 40–45 | Tus preguntas | Curiosidad 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ón | Nivel (0–3) | Evidencia | Siguiente 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.