Kotlin Lab

Aprender / Fluidez Android — sonar como un mid nativo

Fluidez Android — sonar como un mid nativo

Si no tienes claro qué es una Activity, qué pasa al girar el móvil o qué es process death, lee antes docs/ANDROID_BASICS.md.

FLUTTER_TO_KOTLIN.md te enseña a escribir Kotlin. Este documento te enseña a hablarlo: la plataforma que Flutter te escondía, los idioms que delatan a alguien que viene de Dart, la jerga que se usa en dailies y code reviews, y cómo responder sin que se note el acento.

Regla general: un mid Android no se distingue por saber más APIs, sino por saber qué pasa debajo (ciclo de vida, hilos, process death, recomposición) y nombrar los trade-offs.


1. La plataforma que Flutter te escondía

En Flutter tenías un único Activity (FlutterActivity) y un motor que pintaba todo. En Android nativo, la plataforma está en primer plano.

1.1 Componentes de una app

ComponenteQué esEquivalente mental Flutter
ActivityPunto de entrada con ventana. En apps Compose modernas suele haber una sola (MainActivity).FlutterActivity
FragmentTrozo de UI con su propio ciclo de vida. Legacy en apps nuevas, pero lo verás en código existente.—
ServiceTrabajo sin UI. Hoy casi siempre se sustituye por WorkManager o foreground services muy concretos.flutter_background_service
BroadcastReceiverRecibe eventos del sistema (batería, red, boot).plugins de plataforma
ContentProviderExpone datos a otras apps. También lo usan librerías para autoinicializarse (App Startup).—
ApplicationSingleton del proceso. Aquí va @HiltAndroidApp.main()

Todo lo anterior se declara en AndroidManifest.xml (≈ Info.plist + permisos + entry points). Si no está en el manifest, el sistema no lo conoce.

1.2 Context

El BuildContext de Flutter es un handle al árbol de widgets. El Context de Android es un handle al sistema: recursos, ficheros, servicios, tema, permisos.

  • applicationContext: vive lo que vive el proceso. Úsalo para singletons (Room, DataStore, OkHttp cache).

  • Activity context: tiene tema y ventana; necesario para diálogos o inflar UI. Nunca lo guardes en un singleton ni en un ViewModel → memory leak.

  • En Compose: LocalContext.current.

  • Frase de mid: "Al ViewModel no le paso Context; si necesito recursos, mapeo a un UiText o uso AndroidViewModel como último recurso."

1.3 Ciclo de vida de la Activity

onCreate → onStart → onResume   (visible e interactiva)
          onPause → onStop      (en background)
          onDestroy             (rotación, finish o el sistema la mata)
  • Rotación / cambio de configuración (idioma, tema oscuro, tamaño de ventana): la Activity se destruye y recrea. El ViewModel sobrevive; el estado de remember no.

  • Lifecycle + LifecycleOwner permiten observar estos estados. repeatOnLifecycle(STARTED) y collectAsStateWithLifecycle() paran la recolección en background para no gastar batería ni actualizar UI invisible.

  • Equivalente Flutter: WidgetsBindingObserver.didChangeAppLifecycleState, pero en Android afecta a mucho más.

1.4 Process death (la pregunta trampa)

Android puede matar el proceso de tu app en background para liberar memoria. Cuando el usuario vuelve, el sistema recrea la Activity en la misma pantalla, pero:

Sobrevive a rotaciónSobrevive a process death
ViewModel❌
rememberSaveable✅
SavedStateHandle (en el ViewModel)✅
back stack de navegación✅
Room / DataStore✅ (está en disco)
variables en object / singletons❌

Cómo probarlo: app en background y adb shell am kill <package>, o "Don't keep activities" en opciones de desarrollador.

Frase de mid: "El ViewModel sobrevive a cambios de configuración, no a process death. Para el input del usuario y los filtros uso SavedStateHandle; para datos, la fuente de verdad es Room."

1.5 Hilos

  • Main thread (UI thread): todo lo que toca la UI. Si lo bloqueas >5 s en un input → ANR (Application Not Responding). En Flutter el equivalente era el jank en el isolate principal.

  • En coroutines: Dispatchers.Main (UI), Dispatchers.IO (disco/red, pool grande), Dispatchers.Default (CPU, núcleos).

  • Regla de oro: las funciones suspend deben ser main-safe. El que hace I/O se encarga de withContext(Dispatchers.IO); el que llama no tiene que saberlo. Retrofit y Room ya lo hacen por ti.

  • Inyecta los dispatchers (@IoDispatcher) para poder sustituirlos por StandardTestDispatcher en tests.

  • Intent: mensaje para arrancar algo. Explícito (una clase concreta) o implícito (una acción: "abre esta URL", "comparte este texto").

  • Deep links: <intent-filter> en el manifest + Navigation Compose. App Links = deep links verificados con assetlinks.json.

  • Permisos peligrosos (cámara, ubicación, notificaciones desde Android 13) se piden en runtime: rememberLauncherForActivityResult(RequestPermission()). Sustituye a permission_handler.

  • ActivityResult API sustituye al viejo startActivityForResult.

1.7 Recursos

  • res/values/strings.xml → stringResource(R.string.x). Equivalente a intl/ARB. Plurales con pluralStringResource.

  • Cualificadores: values-es/, values-night/, drawable-xxhdpi/. El sistema elige la carpeta según la configuración.

  • R es la clase generada con los IDs de recursos. "No hardcodees strings, sácalos a recursos" es un comentario de review clásico.


2. Build, Gradle y release

ConceptoQué esFlutter
settings.gradle.ktsLista de módulosworkspace
libs.versions.tomlVersion catalog: versiones centralizadaspubspec.yaml (dependencies)
build.gradle.kts del móduloPlugins, android {}, dependenciaspubspec.yaml
implementation vs apiapi expone la dependencia a quien te consume; implementation no (compila más rápido)—
Convention plugins (build-logic/)Configuración compartida entre módulos—
minSdk / targetSdk / compileSdkMínimo soportado / comportamiento que aceptas / APIs con las que compilasminSdkVersion en android/
Build typesdebug / release--debug / --release
Product flavorsdev / staging / prod--flavor
KSPProcesador de anotaciones (Room, Hilt, Moshi). Sustituye a kapt.build_runner
R8Minifica, ofusca y elimina código muerto en release. Reglas en proguard-rules.pro.--obfuscate
AABAndroid App Bundle: lo que subes a Play; Play genera APKs por dispositivo.flutter build appbundle
Baseline ProfilesPrecompilan las rutas críticas → mejor arranque.—

Release en Google Play: firma con upload key (Play App Signing), tracks internal → closed → open → production, staged rollout (5 %, 20 %, 100 %) y monitorizar crash-free rate en Crashlytics / Android Vitals antes de subir el porcentaje.

Frase de mid: "Si un crash solo aparece en release, lo primero que miro es R8: una clase serializada por reflexión sin @Keep o sin regla."


3. Idioms de Kotlin que delatan el acento Dart

3.1 Scope functions

FunciónReferenciaDevuelveUso típico
letitresultado lambdax?.let { … } — trabajar con un nullable
runthisresultado lambdacalcular algo a partir de un objeto
with(x)thisresultado lambdavarias llamadas sobre el mismo objeto
applythisel objetoconfigurar: Intent().apply { putExtra(…) } — el cascade .. de Dart
alsoitel objetoefectos laterales: logging, validación

Errores típicos: anidar tres let; usar ?.let { } ?: run { } como if/else (si el let devuelve null, se ejecuta también el run). Si un if se lee mejor, usa if.

3.2 Extensiones

fun Long.formatCents(): String = "%.2f €".format(this / 100.0)
val Account.isOverdrawn: Boolean get() = balanceCents < 0

Equivalente a extension en Dart. Se resuelven estáticamente (no hay polimorfismo). Úsalas para mappers (fun AccountDto.toDomain()), no para meter lógica de negocio en tipos del framework.

3.3 value class y typealias

@JvmInline value class AccountId(val raw: String)
typealias Cents = Long
  • value class: tipo distinto en compilación, sin coste en runtime. Evita pasar un userId donde va un accountId. Equivale a lo que en Dart harías con extension type.

  • typealias: solo un alias, no da seguridad de tipos.

3.4 object, companion object, lateinit, lazy

  • object Foo: singleton. companion object: lo que en Dart serían miembros static y factories (fun from(dto)).

  • lateinit var: "lo inicializo antes de usarlo" (inyección de campos, tests). Solo var, no nullable, no primitivos.

  • val x by lazy { … }: se calcula al primer acceso, thread-safe por defecto. Equivalente a late final x = … de Dart.

3.5 Delegación

class LoggingRepo(private val inner: AccountRepository) : AccountRepository by inner

by implementa la interfaz delegando; tú sobrescribes solo lo que cambia. También para propiedades: by lazy, by mutableStateOf, by viewModels(), by Delegates.observable.

3.6 Más idioms

  • when exhaustivo sobre sealed → sin else; añadir un caso nuevo rompe la compilación (es lo que quieres).

  • require(x > 0) { "…" } para argumentos, check(state) para estado, error("…") para lanzar. Equivalen a assert/ArgumentError.

  • buildList { }, buildString { }, mapNotNull, associateBy, groupBy, partition, firstOrNull, orEmpty().

  • takeIf { } / takeUnless { }: value.takeIf { it.isNotBlank() } ?: default.

  • Destructuring: val (id, name) = account, for ((key, value) in map).

  • String templates: "$name tiene ${items.size}".

  • Named + default args sustituyen al builder pattern y a los constructores con nombre de Dart.

  • inline fun <reified T>: acceder al tipo genérico en runtime (T::class). Dart no tiene borrado de tipos; la JVM sí.

  • runCatching atrapa CancellationException. En código con coroutines, relánzala o usa un try/catch explícito. Pregunta favorita de entrevista.

3.7 Interop con Java (la vacante pide Java)

  • Tipos plataforma (String!): Kotlin no sabe si son nullables. Anótalos en el borde o trátalos como nullables.

  • @JvmStatic, @JvmOverloads, @JvmField, @Throws: hacer que Kotlin se vea natural desde Java.

  • SAM conversions: una interfaz Java de un método acepta una lambda Kotlin.

  • Kotlin no tiene checked exceptions; Java sí.

  • Con saber leer código Java legacy (un Fragment con LiveData, por ejemplo) suele bastar; rara vez te pedirán escribir Java nuevo.


4. Coroutines: el vocabulario de un mid

  • Structured concurrency: toda coroutine vive en un scope; si el scope se cancela, se cancelan las hijas. Nada de GlobalScope.

  • viewModelScope (se cancela en onCleared), lifecycleScope, rememberCoroutineScope() en Compose.

  • launch (fire and forget, devuelve Job) vs async (devuelve Deferred, await()). Paralelismo: coroutineScope { val a = async {…}; val b = async {…}; a.await() + b.await() } ≈ Future.wait.

  • SupervisorJob / supervisorScope: el fallo de una hija no cancela a las hermanas.

  • Cancelación cooperativa: solo se cancela en puntos de suspensión (delay, yield, ensureActive()).

  • CoroutineExceptionHandler: solo funciona en coroutines raíz con launch.

  • Cold vs hot: Flow es frío (se ejecuta por cada colector, como un Stream single-subscription que arranca al escuchar). StateFlow es caliente, tiene valor y hace conflation (se salta valores intermedios). SharedFlow es caliente sin valor, configurable con replay.

  • Eventos de un solo uso (navegar, snackbar): Channel + receiveAsFlow() o, mejor según Google, modelarlos como estado y "consumirlos".

  • stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), initial): los 5 s sobreviven a una rotación sin reiniciar el upstream. Pregunta muy frecuente.

  • Operadores que debes nombrar sin pensar: map, filter, combine, flatMapLatest (≈ switchMap de RxDart), debounce, distinctUntilChanged, catch, onStart, retryWhen, flowOn.

  • Tests: runTest, StandardTestDispatcher, advanceUntilIdle(), Turbine (flow.test { awaitItem() }).


5. Compose por dentro

  • Composición → layout → dibujo: tres fases. Leer estado en una fase posterior (p. ej. Modifier.offset { } con lambda) evita recomponer.

  • Recomposición: Compose vuelve a ejecutar solo las funciones cuyos parámetros cambiaron. Puede saltarse (skip) funciones si sus parámetros son estables.

  • Estabilidad: List<T> es inestable para el compilador (podría ser mutable). Soluciones: kotlinx.collections.immutable (ImmutableList), @Immutable/@Stable, o strong skipping mode (activo por defecto desde Kotlin 2.0.20), que relaja mucho este problema.

  • remember(key) { } ≈ memoizar en build. derivedStateOf cuando un estado deriva de otro que cambia más a menudo (p. ej. "mostrar botón si firstVisibleItemIndex > 0").

  • Side effects: LaunchedEffect(key) (≈ initState + didUpdateWidget con coroutine), DisposableEffect (≈ dispose), SideEffect, rememberUpdatedState, produceState, snapshotFlow.

  • CompositionLocal ≈ InheritedWidget / Theme.of(context).

  • key en LazyColumn ≈ Key en ListView.builder. Sin key, se pierde el estado al reordenar.

  • Herramientas: Layout Inspector con recomposition counts, Compose compiler reports para ver estabilidad.

  • Interop: AndroidView para meter una View clásica (mapas, WebView) dentro de Compose; ComposeView al revés.


6. Arquitectura: la jerga que vas a oír

TérminoQué significa en la práctica
MVVMViewModel expone StateFlow<UiState>; la UI observa y manda eventos. Lo que pide la vacante.
UDF (Unidirectional Data Flow)Estado baja, eventos suben. Igual que en Bloc.
MVIMVVM + un único UiState + eventos como sealed interface Intent/Action procesados por un reducer. Como Bloc con eventos.
Clean ArchitectureCapas data / domain / ui. El dominio no depende de Android.
UseCase / InteractorClase con una única operación de negocio (operator fun invoke). Opcional según la guía oficial: úsalo si reutilizas lógica o combinas repos.
RepositoryÚnica puerta a los datos; decide entre red y caché.
Single source of truth (SSOT)Room es la verdad; la red solo la actualiza. La UI observa Room. Offline-first.
DataSourceRemoteDataSource (Retrofit), LocalDataSource (Room/DataStore).
DTO / Entity / Domain modelTres modelos con mappers entre ellos.
Modularización:app, :core:*, :feature:*. Builds incrementales más rápidos, límites claros. Que un feature no dependa de otro feature.
DI con Hilt@HiltAndroidApp, @AndroidEntryPoint, @HiltViewModel, @Inject constructor, @Module + @Provides/@Binds, scopes @Singleton / @ViewModelScoped. Equivale a get_it + injectable, pero validado en compilación.
KoinDI alternativo, sin generación de código, resolución en runtime. Conocerlo de nombre.
Paging 3Listas paginadas con PagingSource / RemoteMediator.
WorkManagerTrabajo diferible y garantizado (sync, subir ficheros) que sobrevive a reinicios.
DataStoreSustituto de SharedPreferences (≈ shared_preferences), asíncrono con Flow. Proto DataStore si quieres tipado.

Equivalencias rápidas de librerías:

FlutterAndroid
dio / httpRetrofit + OkHttp (interceptors ≈ interceptors de dio)
json_serializable / freezedkotlinx.serialization o Moshi
sqflite / driftRoom
flutter_bloc / riverpodViewModel + StateFlow
go_routerNavigation Compose (type-safe con @Serializable)
get_itHilt
cached_network_imageCoil
flutter_secure_storageEncryptedSharedPreferences (deprecado) / Keystore + Tink
local_authBiometricPrompt
firebase_crashlyticsCrashlytics (igual)
mocktailMockK (o fakes, preferidos por Google)
integration_testEspresso / Compose UI test / UI Automator
golden testsPaparazzi / Roborazzi (screenshot testing)
very_good_analysisDetekt + ktlint / Android Lint

7. Testing: lo que esperan de un mid

  • Pirámide: muchos unit tests JVM (dominio, ViewModel), algunos de integración (Room in-memory, MockWebServer), pocos de UI.

  • Fakes > mocks: Google recomienda FakeAccountRepository con un MutableStateFlow interno antes que MockK.

  • ViewModel: MainDispatcherRule que reemplaza Dispatchers.Main por un TestDispatcher.

  • Compose UI: createComposeRule(), onNodeWithText, onNodeWithTag, performClick, assertIsDisplayed. Semántica = accesibilidad + testabilidad.

  • Espresso para Views clásicas; onView(withId(...)).perform(click()). Lo pide la vacante: debes saber nombrarlo y leerlo.

  • Robolectric: tests de Android en la JVM sin emulador.

  • Debugging: Logcat, breakpoints con evaluate, LeakCanary para leaks, Android Profiler (CPU, memoria), StrictMode para I/O en main thread.


8. Jerga del día a día

Lo que oirás en dailies, PRs y Slack:

TérminoSignificado
Recomposition / recomposeCompose vuelve a ejecutar un composable. "Esto recompone en cada frame."
Hoist the stateSubir el estado al llamador.
CollectSuscribirse a un Flow. "Lo colecto con lifecycle."
Main-safeSe puede llamar desde el hilo principal sin bloquear.
LeakAlgo retiene una Activity muerta.
ANRLa app no responde (main thread bloqueado).
Config changeRotación, tema, idioma… recrea la Activity.
Process deathEl sistema mató el proceso en background.
Edge-to-edge / insetsDibujar detrás de barras del sistema y respetar WindowInsets. Obligatorio con targetSdk 35.
Cold start / warm startArranque con proceso nuevo / con proceso vivo.
JankFrames perdidos (igual que en Flutter).
Obfuscation / mapping fileR8 renombra clases; el mapping.txt traduce los stack traces.
FlavorVariante de build.
Version catalog / TOMLlibs.versions.toml.
Gradle syncEl IDE relee la configuración. "¿Has hecho sync?"
Clean build./gradlew clean. El "flutter clean" de toda la vida.
NitComentario menor de review.
LGTMLooks good to me.
Flaky testTest que falla a veces (casi siempre: dispatchers o tiempo real).
Staged rollout / halt rolloutPublicación gradual en Play / pararla.
Feature flag / remote configActivar funcionalidades sin release.
KMPKotlin Multiplatform: compartir dominio/datos con iOS. Pregunta probable viniendo de Flutter.

9. Frases: acento Flutter → acento nativo

Lo que dirías viniendo de FlutterLo que dice un mid Android
"Hago setState cuando llegan los datos.""El ViewModel expone un StateFlow<UiState> y la pantalla lo colecta con collectAsStateWithLifecycle."
"El widget se reconstruye.""El composable se recompone; si los parámetros son estables, se salta."
"Lo guardo en el provider para que no se pierda.""Va en SavedStateHandle para sobrevivir a process death."
"Hago un Future.wait.""Lanzo dos async dentro de coroutineScope y hago await."
"Escucho el stream en initState.""Lo colecto en un LaunchedEffect o, mejor, lo expongo como estado desde el ViewModel."
"Uso compute para no bloquear.""Cambio a Dispatchers.Default con withContext."
"Lo pongo en un singleton global.""Lo inyecto con Hilt con scope @Singleton."
"Pido el permiso con permission_handler.""Uso rememberLauncherForActivityResult con RequestPermission y manejo el rationale."
"Hago un platform channel."(No existe; ya estás en la plataforma.)
"El hot reload…""Uso Live Edit / previews con @Preview y @PreviewParameter."

10. Preguntas de entrevista

Las preguntas viven en un único banco: docs/ASSESSMENT.md → "Preguntas de autoevaluación". Cada respuesta enlaza a la sección de este documento que la explica (→ §x).

Las marcadas con ⭐ son las que separan junior de mid. Si vas justo de tiempo, repasa solo esas. Incluye la de KMP frente a Flutter, que te van a hacer casi seguro.


11. Plan rápido si la entrevista técnica está cerca

El plan día a día está en el README.md, sección "Ruta para la entrevista de DKB". Empieza por docs/ANDROID_BASICS.md: este documento da por sabido lo que se explica allí.

Después de la entrevista, vuelve a CURRICULUM.md para el ritmo largo.

Fuente: docs/ANDROID_FLUENCY.md