AND

Domina la descripción del puesto

Guía de Estudio

Cada requisito que un rol senior de Android comúnmente pide — explicado a profundidad senior, y luego ligado a cómo lo aplicarías en un equipo real. Lee el concepto, luego la línea “en la práctica” para que puedas hablar desde la experiencia.

Profundidad sobre trivialidades Las entrevistas senior y staff de Android profundizan en cómo razonas — por qué StateFlow sobre LiveData, cuándo la recomposición realmente te cuesta, cómo sobrevives la muerte del proceso, y dónde las coroutines filtran. Cada sección a continuación empareja el concepto con una nota de en la práctica para que puedas responder desde una decisión de ingeniería real, no una definición.

01 · Fundamentos del lenguaje Kotlin

Lo que buscan: Kotlin idiomático, de producción — no Java escrito en sintaxis de Kotlin. Fluidez con val vs var, inmutabilidad por defecto, seguridad de nulos (?., ?:, !! como mala práctica), data classes, jerarquías sealed para modelar estado, y las funciones de ámbito (let, run, with, apply, also).

  • Prefiere val y colecciones inmutables; expón List, almacena MutableList privadamente.
  • Modela estado exhaustivo con sealed interface + when (sin rama else → el compilador impone completitud).
  • Delegación: by lazy, by viewModels(), delegados de propiedades, y delegación de clases con by.
  • inline + reified para conservar info de tipo genérico en tiempo de ejecución; entiende el costo (tamaño de código) de inlining.
En la práctica Una respuesta senior vincula seguridad de nulos con tasa de crash: el sistema de tipos empuja la anulabilidad al límite (parsing, tipos de plataforma de Java) para que el resto de la app sea no nulo por construcción. !! en una revisión es una señal de alarma — convierte una garantía en tiempo de compilación en un crash en tiempo de ejecución.

02 · Coroutines y concurrencia estructurada

Lo que buscan: coroutines profundas — no solo launch { }. La concurrencia estructurada significa que cada coroutine tiene un CoroutineScope padre; cancelar el scope cancela sus hijos, y un fallo hijo cancela hermanos (a menos que un SupervisorJob los aísle).

  • Las funciones suspend deben ser seguras para el principal: pueden llamarse en el hilo principal e internamente cambiar con withContext(Dispatchers.IO).
  • Dispatchers: Main (UI), IO (I/O bloqueante, pool grande), Default (trabajo de CPU), Unconfined (rara vez).
  • La cancelación es cooperativa: verifica isActive / llama ensureActive(), y nunca tragues CancellationException.
  • coroutineScope vs supervisorScope; async/await para descomposición paralela.
Fija esto viewModelScope y lifecycleScope son scopes estructurados conectados a un ciclo de vida — se cancelan automáticamente, lo cual es por qué casi nunca necesitas GlobalScope (que filtra porque nada lo cancela).
En la práctica El encuadre senior: "Dejo que el scope sea dueño de la cancelación." Una llamada de red iniciada en viewModelScope se cancela cuando el ViewModel se limpia, así que un cambio de configuración o retroceso no filtra una solicitud ni actualiza una UI muerta.

03 · Flow, StateFlow y SharedFlow

Lo que buscan: streams reactivos bien hechos. Flow es un stream asíncrono frío; los operadores (map, filter, combine, flatMapLatest) se ejecutan en el contexto del collector a menos que uses flowOn.

StateFlowSharedFlow
Mantiene valorSiempre (requiere inicial)Replay opcional
ConflaciónConflado, distinto-hasta-que-cambieBuffer configurable
Usar paraEstado observable de UIEventos únicos (navegar, toast)
  • Recolecta con repeatOnLifecycle(STARTED) (o collectAsStateWithLifecycle() en Compose) para que la recolección se detenga en segundo plano — no launchWhenStarted (obsoleto, mantiene el upstream caliente).
  • stateIn(scope, SharingStarted.WhileSubscribed(5000), initial) comparte un flow frío como estado y detiene el upstream 5s después del último suscriptor — sobrevive rotación sin re-obtener.
En la práctica El error clásico: recolectar un flow en onCreate con un launch simple mantiene el collector vivo en segundo plano, desperdiciando trabajo y arriesgando actualizaciones de UI obsoletas. repeatOnLifecycle es la solución que los entrevistadores quieren escuchar.

04 · Jetpack Compose — composición y recomposición

Lo que buscan: el modelo mental. Un @Composable es una función que describe UI a partir del estado; Compose construye un árbol y recomposa (reinvoca) solo las funciones cuyas entradas cambiaron. No hay jerarquía de vistas que mutar — cambias el estado y el framework reconcilia.

  • Ámbito de recomposición: el composable contenedor más pequeño que lee un estado dado. Leer estado bajo en el árbol mantiene la recomposición local.
  • Estabilidad: Compose omite un composable si sus parámetros son @Stable/@Immutable e inmutables. Tipos inestables (ej. List de una fuente no estable) impiden el skip → usa ImmutableList / kotlinx.collections.immutable.
  • Fases: Composición → Layout → Dibujo. Difiere lecturas a la fase más tardía (ej. lambda Modifier.offset { }) para omitir recomposición en scroll/animación.
Concepto nuevo Skip de agujero de dona: un padre puede recomponer sin recomponer hijos estables. Internaliza 'qué lee este estado' y puedes explicar jank con precisión en lugar de memoizar a ciegas.
En la práctica Activa las métricas del compilador de Compose / contadores de recomposición del Layout Inspector. Un senior depura una lista que se recomposición de más encontrando el parámetro inestable, no esparciendo remember.

05 · Estado y efectos secundarios en Compose

Lo que buscan: state hoisting y las APIs de efectos secundarios. remember cachea entre recomposiciones; rememberSaveable sobrevive cambio de configuración/muerte del proceso. Eleva el estado hacia arriba (composables sin estado toman value + onValueChange) para que la UI sea reutilizable y testeable.

  • LaunchedEffect(key) — ejecuta un bloque suspend vinculado a la composición; se reinicia cuando key cambia, se cancela al salir.
  • rememberCoroutineScope() — lanza desde callbacks (ej. clic en botón), con scope de la composición.
  • DisposableEffect — registra/desregistra (listeners, observers) con un onDispose.
  • derivedStateOf — calcula desde otro estado y solo recomposa cuando el resultado cambia (ej. showButton = scrollOffset > 0).
  • produceState — conecta una fuente asíncrona no-Compose a un State; snapshotFlow — convierte estado de Compose en un Flow.
En la práctica La trampa de la entrevista: lanzar una coroutine directamente en el cuerpo del composable en lugar de LaunchedEffect — se dispara en cada recomposición. Decir "los efectos pertenecen a manejadores de efectos con key correcta" señala madurez real de Compose.

06 · Ciclo de vida de Activity / Fragment y cambios de configuración

Lo que buscan: el ciclo de vida al dedillo. Activity: onCreate → onStart → onResume (primer plano) → onPause → onStop → onDestroy. Un cambio de configuración (rotación, modo oscuro, idioma, escala de fuente) destruye y recrea la Activity por defecto.

  • El ciclo de vida de la vista del Fragment es separado del fragment: usa viewLifecycleOwner para observadores, o filtras cuando la vista se destruye pero el fragment no.
  • No pelees contra cambios de configuración con android:configChanges a menos que realmente controles el redibujado — prefiere sobrevivir estado en un ViewModel.
  • onSaveInstanceState(Bundle) persiste estado pequeño de UI a través de recreación y muerte del proceso (parcelable, < ~1MB).
En la práctica Respuesta senior: "La rotación no es especial — es la misma ruta de recreación que la muerte del proceso con más estado retenido. Si mi estado sobrevive muerte del proceso vía SavedStateHandle, la rotación es gratis."

07 · ViewModel, SavedStateHandle y muerte del proceso

Lo que buscan: dónde vive el estado y cómo sobrevive. Un ViewModel sobrevive cambios de configuración (con scope al ViewModelStoreOwner) pero se limpia cuando el propietario realmente termina. No sobrevive muerte del proceso por sí solo — el OS puede matar una app en segundo plano para recuperar memoria.

  • SavedStateHandle es el puente: un mapa de valores respaldado por estado de instancia guardado, así que los valores sobreviven muerte del proceso. Inyectalo en el ViewModel y expón estado vía getStateFlow(key, default).
  • Nunca mantengas una referencia Context/View/Activity en un ViewModel (filtra). Usa el contexto de aplicación de AndroidViewModel solo si es inevitable.
  • Prueba la ruta de restauración: mata el proceso desde herramientas de desarrollo ("No conservar actividades") y confirma que la pantalla se reconstruye correctamente.
Fija esto El ViewModel sobrevive rotación; SavedStateHandle sobrevive muerte del proceso; una base de datos/DataStore sobrevive todo. Nombrar los tres niveles es la señal senior.
En la práctica El fallo realista: una pantalla de búsqueda pierde la consulta después de que el OS mate la app en segundo plano. Pon la consulta en SavedStateHandle y se restaura — la solución que los entrevistadores están escuchando.

08 · Arquitectura — MVVM, MVI y Clean Architecture

Lo que buscan: una arquitectura defendible con flujo de datos unidireccional. MVVM: el ViewModel expone estado observable; la Vista lo renderiza y envía eventos hacia arriba. MVI condensa esto en un único UiState inmutable + un stream de Intent/Action reducido a nuevo estado — genial para pantallas complejas y con muchos experimentos.

  • Capas de Clean Architecture: UIDominio (casos de uso, puro Kotlin, sin Android) → Datos (repositorios, fuentes). Las dependencias apuntan hacia adentro; el dominio no sabe nada de Retrofit o Room.
  • Fuente única de verdad: el repository decide caché vs red; la UI nunca llama a la API directamente.
  • Modela el estado como un solo objeto: data class UiState(val items: List, val loading: Boolean, val error: String?) — renderiza todo, evita booleanos dispersos.
En la práctica El pragmatismo gana: "Uso MVVM con un estado inmutable único por pantalla y casos de uso solo donde la lógica de dominio no es trivial — no añado un caso de uso que solo reenvía una llamada a un repo." Esa sutileza se lee como juicio de staff, no dogma.

09 · Inyección de dependencias — Hilt y Dagger

Lo que buscan: DI para mantener el código testeable y desacoplado. Hilt se apoya en Dagger y genera componentes vinculados a ciclos de vida de Android: @HiltAndroidApp, @AndroidEntryPoint, @HiltViewModel, y scopes (@Singleton, @ActivityRetainedScoped, @ViewModelScoped).

  • @Module + @Provides para cosas que no posees (Retrofit, OkHttp); @Binds para interfaz→impl.
  • Inyección por constructor (@Inject constructor) donde puedas — es la más testeable.
  • Qualifiers (@Named / custom) desambigúan múltiples bindings del mismo tipo (ej. dos clientes OkHttp).
Fija esto El punto de la DI no es "los objetos se crean por mí" — es la inversión de dependencias: el código de alto nivel depende de interfaces, así que los tests intercambian un repository falso en una línea. Enmarca la DI como una herramienta de testeabilidad y modularización.
En la práctica El grafo en tiempo de compilación de Hilt significa que un binding faltante es un error de compilación, no un crash en tiempo de ejecución — una ventaja real que debes nombrar sobre patrones de service-locator.

10 · Persistencia — Room y DataStore

Lo que buscan: almacenamiento local elegido correctamente. Room es el ORM de SQLite: @Entity, @Dao, @Query, con verificación SQL en tiempo de compilación y tipos de retorno Flow para lecturas reactivas. DataStore reemplaza SharedPreferences: Preferences DataStore (clave-valor) o Proto DataStore (tipado), ambos asíncronos y transaccionales.

  • Los DAOs de Room que retornan Flow<List<T>> emiten en cada cambio — la columna vertebral de offline-first.
  • Migraciones: proporciona un Migration o arriesga un crash en cambio de esquema; exporta el esquema para tests.
  • Nunca hagas disco I/O en el hilo principal — los DAOs suspend de Room y DataStore son seguros para el principal por diseño.
En la práctica "SharedPreferences es síncrono y silenciosamente hace I/O en el hilo principal (apply() difiere la escritura a disco pero el commit en memoria aún bloquea). DataStore corrige eso con un Flow + coroutines." Ese contraste es una pregunta senior frecuente.

11 · Red — Retrofit, OkHttp y serialización

Lo que buscan: una capa de red robusta. Retrofit define una interfaz de API tipada; OkHttp es el motor con el pool de conexiones, caché e interceptores. Serializa con Moshi o kotlinx.serialization.

  • Interceptores: uno de aplicación para headers de auth/logging; uno de red para control de caché. Añade un Authenticator de refresh de token para 401s.
  • Modela resultados con un tipo sealed Result/Either — nunca dejes que excepciones crudas lleguen a la UI.
  • Mapea DTOs → modelos de dominio en el repository; los DTOs nunca se filtran a Compose.
  • Caché HTTP de OkHttp + ETags para GETs condicionales; reintenta solo llamadas idempotentes con backoff.
En la práctica Offline-first: el repository lee de Room (fuente de verdad) y refresca desde la red, escribiendo de vuelta a la DB para que la UI actualice vía su Flow — el usuario nunca mira un spinner, con semántica stale-while-revalidate.

12 · Navegación y deep linking

Núcleo: Navigation-Compose modela la app como un grafo de destinos con rutas tipadas (rutas type-safe vía objetos de ruta @Serializable en Navigation 2.8+). El NavController posee el back stack; pasas argumentos y haces pop con resultados.

  • Eleva el NavController en la raíz del grafo; pasa lambdas (onNavigateToDetail) a pantallas para que los composables se mantengan desacoplados de navegación y testeables.
  • Deep links: declara <intent-filter> + android:autoVerify para App Links (https verificado) y un navDeepLink que mapea la URI a un destino.
  • Gráficos anidados para módulos de features; SavedStateHandle recibe args de navegación dentro del ViewModel.
En la práctica El gating de auth es una decisión de grafo, no una pantalla oculta: un usuario no autenticado recibe el grafo de auth como destino de inicio. App Links necesitan el assetlinks.json en tu dominio — un paso que la gente olvida.

13 · Trabajo en segundo plano — WorkManager, Services y broadcasts

Lo que buscan: la herramienta correcta para trabajo fuera de pantalla. WorkManager es el predeterminado para trabajo diferido, garantizado (sincronización, subida) — sobrevive muerte del proceso y reinicio, respeta restricciones (red, carga), y retrocede en fallo.

  • OneTimeWorkRequest vs PeriodicWorkRequest (intervalo mínimo de 15 min); encadena con beginWith().then(); trabajo único para deduplicar.
  • Services de primer plano para trabajo visible continuo por el usuario (reproducción, navegación) — requieren una notificación y, en Android 14+, un foregroundServiceType declarado.
  • Límites modernos de segundo plano: JobScheduler/WorkManager sobre wake-locks implícitos; BroadcastReceiver para eventos del sistema, pero la mayoría de broadcasts implícitos están restringidos.
En la práctica "No uses un Service para una sincronización de red puntual — eso es WorkManager. Usa un service de primer plano solo cuando el usuario pueda verlo." Conocer el límite (y Doze/App Standby) es la señal senior.

14 · Rendimiento — jank, timing de frames y baseline profiles

Núcleo: la métrica es frames renderizados dentro del presupuesto de 16.6ms (60fps; 8.3ms a 120Hz). Jank = un frame perdido, surfaced por JankStats, la librería Macrobenchmark, y trazas de Perfetto.

  • Baseline Profiles: distribuye pistas de compilación AOT para rutas calientes para que las primeras ejecuciones no se interpreten — victorias mayores en arranque en frío y scroll, generados vía Macrobenchmark.
  • Compose: evita parámetros inestables, difiere lecturas de estado a layout/draw, usa keys de LazyColumn, y remember computaciones costosas.
  • Disciplina del hilo principal: sin I/O, sin parseo JSON grande, sin decodificación de bitmap en el hilo de UI.
Concepto nuevo Mide en un build de producción con R8 habilitado — los builds de debug son engañosamente lentos y Compose no está completamente optimizado. Macrobenchmark maneja una instalación real.
En la práctica "Perfilo con Perfetto/Macrobenchmark, arreglo el frame caliente demostrado, luego añado un baseline profile y un benchmark para protegerlo." Perfilar primero vence a adivinar — la línea que los entrevistadores quieren.

15 · Memory leaks, ANRs y profiling

Núcleo: dos modos de fallo. Los leaks retienen objetos que el GC debería liberar — el leak clásico de Android es un Context/View retenido más allá de su ciclo de vida (refs estáticos, inner classes, callbacks, un Handler con runnables retrasados). Los ANRs se disparan cuando el hilo principal está bloqueado > 5s (despacho de entrada) — casi siempre I/O en el hilo principal o contención de locks.

  • LeakCanary en debug detecta automáticamente instancias retenidas y vuelca la cadena de referencias — nómbralo como tu primera herramienta.
  • Memory Profiler de Android Studio + heap dumps para crecimiento acumulativo; trazas de ANR en Play Console / /data/anr/traces.txt.
  • Leak de coroutine común: lanzar en GlobalScope o recolectar un flow sin conciencia del ciclo de vida.
En la práctica Los leaks se presentan como jank lento y acumulativo y crashes OOM después de sesiones largas — los cazas con el tiempo con LeakCanary, no desde un solo stack trace. Los ANR los arreglas sacando trabajo del hilo principal.

16 · Arranque de app y R8 / ProGuard

Núcleo: arranque en frío = fork del proceso → Application.onCreate → primer frame. El presupuesto se gasta en inicialización ansiosa, grafos de dependencias grandes, y trabajo en el hilo principal.

  • Usa la librería App Startup para consolidar y ordenar inicializadores (e inicializar lazy los que no se necesitan al lanzamiento).
  • R8 reduce, optimiza, y ofusca: eliminación de código muerto/recursos corta tamaño y mejora carga; las reglas de keep (-keep) protegen reflexión (Gson, serialización).
  • Difiere trabajo no crítico fuera de la ruta de lanzamiento; muestra contenido rápido y hidrata el resto.
En la práctica "Mido el arranque en frío en un build de producción con Macrobenchmark, recorto Application.onCreate, añado un baseline profile, y mantengo R8 habilitado." Tratar el arranque como un presupuesto defendido es una señal senior.

17 · Testing — JUnit, MockK, Turbine y tests de UI Compose

Lo que buscan: una pirámide de testing real. Muchos tests unitarios rápidos (JUnit + MockK) sobre ViewModels, casos de uso y mappers; menos tests de integración; algunos UI/E2E (Compose test o Espresso) en flujos críticos.

  • Coroutines/Flow: runTest + un TestDispatcher (inyecta el dispatcher, nunca hardcode Dispatchers.IO); aserta emisiones con Turbine.
  • Fakes vs mocks: prefiere un repository fake escrito a mano para tests basados en estado; usa mocks para verificación de interacciones. El exceso de mocking acopla tests a la implementación.
  • Robolectric ejecuta el framework de Android en la JVM (rápido, sin dispositivo); el Compose UI test usa semántica (onNodeWithText, assertIsDisplayed).
En la práctica El patrón de dispatcher inyectable es la señal senior: class Repo(private val io: CoroutineDispatcher) para que los tests pasen StandardTestDispatcher() y controlen el tiempo virtual. Los dispatchers hardcodeados hacen los tests inestables.

18 · Gradle — variantes, catálogos de versiones y modularización

Lo que buscan: puedes razonar sobre la compilación. Variantes de build = tipos de build (debug/release) × product flavors (free/paid, staging/prod). Los catálogos de versiones (libs.versions.toml) centralizan versiones de dependencias.

  • KSP vs kapt: KSP es el procesador de anotaciones nativo de Kotlin — mucho más rápido que kapt (que genera stubs de Java). Room/Hilt/Moshi soportan KSP; migra de kapt para velocidad de build.
  • Modularización: divide por feature (:feature:home) y capa (:core:data) para compilaciones paralelas, compilación incremental más rápida, y límites forzados.
  • Convention plugins (build-logic) eliminan duplicación en configuración de módulos; api vs implementation controla qué se filtra transitivamente.
En la práctica "La modularización compra compilaciones paralelas + incrementales y imposición arquitectónica — un módulo :feature físicamente no puede importar los internos de otra feature." Nombrar velocidad de build y límites es la respuesta de staff.

19 · Seguridad — Keystore, cifrado e integridad

Núcleo: el APK se distribuye al dispositivo y puede ser decompilado — nunca hardcodees secretos. Almacena llaves en el Android Keystore (respaldado por hardware donde esté disponible); cifra datos locales con Jetpack Security (EncryptedSharedPreferences / EncryptedFile) o SQLCipher para Room.

  • Tokens: acceso de vida corta + refresh; puerta a acciones sensibles detrás de BiometricPrompt.
  • Red: Network Security Config + certificate pinning (cuidado con rotación), solo HTTPS, sin texto plano.
  • Play Integrity API certifica una app/dispositivo genuina; ofuscación (R8) y detección de root son defensa en profundidad, no balas de plata.
  • Conoce el vocabulario OWASP MASVS / Mobile Top 10: almacenamiento inseguro, cripto débil, etc.
En la práctica "Analiza el modelo de amenazas en voz alta": secretos-en-bundle, datos en reposo, datos en tránsito, e integridad del dispositivo son cuatro categorías distintas. Nombrarlas muestra madurez de seguridad.

20 · Accesibilidad y la frontera de AI en el dispositivo

Núcleo: distribuye para TalkBack. En Compose, establece Modifier.semantics { contentDescription = ... }, marca encabezados, agrupa nodos relacionados, respeta tamaño de objetivo táctil (48dp), Dynamic Type (unidades sp), y contraste (WCAG AA).

  • Modifier.clickable(onClickLabel=...), stateDescription, y merge de semántica para controles compuestos.
  • Prueba con Accessibility Scanner y aserciones de semántica de Compose.

Frontera: AI generativa en el dispositivo vía Gemini Nano (AICore / APIs ML Kit GenAI) y la API MediaPipe LLM Inference, más TensorFlow Lite (LiteRT) clásico para visión/audio. La compensación: privacidad (los datos se mantienen locales), offline, sin costo de servidor — contra tamaño del modelo, RAM y fragmentación de dispositivos.

En la práctica "Gemini Nano está limitado a dispositivos capaces (Pixel 8+/banderas vía AICore); detecto capacidades y recurro a un modelo de servidor de lo contrario." Razonar sobre la compensación de privacidad/latencia/costo y soporte de dispositivos es la respuesta de staff.