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
| Componente | Qué es | Equivalente mental Flutter |
|---|---|---|
Activity | Punto de entrada con ventana. En apps Compose modernas suele haber una sola (MainActivity). | FlutterActivity |
Fragment | Trozo de UI con su propio ciclo de vida. Legacy en apps nuevas, pero lo verás en código existente. | — |
Service | Trabajo sin UI. Hoy casi siempre se sustituye por WorkManager o foreground services muy concretos. | flutter_background_service |
BroadcastReceiver | Recibe eventos del sistema (batería, red, boot). | plugins de plataforma |
ContentProvider | Expone datos a otras apps. También lo usan librerías para autoinicializarse (App Startup). | — |
Application | Singleton 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).Activitycontext: 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
UiTexto usoAndroidViewModelcomo ú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
Activityse destruye y recrea. ElViewModelsobrevive; el estado derememberno.Lifecycle+LifecycleOwnerpermiten observar estos estados.repeatOnLifecycle(STARTED)ycollectAsStateWithLifecycle()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ón | Sobrevive 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
suspenddeben ser main-safe. El que hace I/O se encarga dewithContext(Dispatchers.IO); el que llama no tiene que saberlo. Retrofit y Room ya lo hacen por ti.Inyecta los dispatchers (
@IoDispatcher) para poder sustituirlos porStandardTestDispatcheren tests.
1.6 Intents, permisos y deep links
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 conassetlinks.json.Permisos peligrosos (cámara, ubicación, notificaciones desde Android 13) se piden en runtime:
rememberLauncherForActivityResult(RequestPermission()). Sustituye apermission_handler.ActivityResultAPI sustituye al viejostartActivityForResult.
1.7 Recursos
res/values/strings.xml→stringResource(R.string.x). Equivalente aintl/ARB. Plurales conpluralStringResource.Cualificadores:
values-es/,values-night/,drawable-xxhdpi/. El sistema elige la carpeta según la configuración.Res 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
| Concepto | Qué es | Flutter |
|---|---|---|
settings.gradle.kts | Lista de módulos | workspace |
libs.versions.toml | Version catalog: versiones centralizadas | pubspec.yaml (dependencies) |
build.gradle.kts del módulo | Plugins, android {}, dependencias | pubspec.yaml |
implementation vs api | api 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 / compileSdk | Mínimo soportado / comportamiento que aceptas / APIs con las que compilas | minSdkVersion en android/ |
| Build types | debug / release | --debug / --release |
| Product flavors | dev / staging / prod | --flavor |
| KSP | Procesador de anotaciones (Room, Hilt, Moshi). Sustituye a kapt. | build_runner |
| R8 | Minifica, ofusca y elimina código muerto en release. Reglas en proguard-rules.pro. | --obfuscate |
| AAB | Android App Bundle: lo que subes a Play; Play genera APKs por dispositivo. | flutter build appbundle |
| Baseline Profiles | Precompilan 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ón | Referencia | Devuelve | Uso típico |
|---|---|---|---|
let | it | resultado lambda | x?.let { … } — trabajar con un nullable |
run | this | resultado lambda | calcular algo a partir de un objeto |
with(x) | this | resultado lambda | varias llamadas sobre el mismo objeto |
apply | this | el objeto | configurar: Intent().apply { putExtra(…) } — el cascade .. de Dart |
also | it | el objeto | efectos 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 unuserIddonde va unaccountId. Equivale a lo que en Dart harías conextension 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 miembrosstaticy factories (fun from(dto)).lateinit var: "lo inicializo antes de usarlo" (inyección de campos, tests). Solovar, no nullable, no primitivos.val x by lazy { … }: se calcula al primer acceso, thread-safe por defecto. Equivalente alate 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
whenexhaustivo sobresealed→ sinelse; 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 aassert/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í.runCatchingatrapaCancellationException. En código con coroutines, relánzala o usa untry/catchexplí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
FragmentconLiveData, 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 enonCleared),lifecycleScope,rememberCoroutineScope()en Compose.launch(fire and forget, devuelveJob) vsasync(devuelveDeferred,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 conlaunch.Cold vs hot:
Flowes frío (se ejecuta por cada colector, como unStreamsingle-subscription que arranca al escuchar).StateFlowes caliente, tiene valor y hace conflation (se salta valores intermedios).SharedFlowes caliente sin valor, configurable conreplay.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(≈switchMapde 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 enbuild.derivedStateOfcuando un estado deriva de otro que cambia más a menudo (p. ej. "mostrar botón sifirstVisibleItemIndex > 0").Side effects:
LaunchedEffect(key)(≈initState+didUpdateWidgetcon coroutine),DisposableEffect(≈dispose),SideEffect,rememberUpdatedState,produceState,snapshotFlow.CompositionLocal ≈
InheritedWidget/Theme.of(context).keyenLazyColumn≈KeyenListView.builder. Sin key, se pierde el estado al reordenar.Herramientas: Layout Inspector con recomposition counts, Compose compiler reports para ver estabilidad.
Interop:
AndroidViewpara meter unaViewclásica (mapas, WebView) dentro de Compose;ComposeViewal revés.
6. Arquitectura: la jerga que vas a oír
| Término | Qué significa en la práctica |
|---|---|
| MVVM | ViewModel 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. |
| MVI | MVVM + un único UiState + eventos como sealed interface Intent/Action procesados por un reducer. Como Bloc con eventos. |
| Clean Architecture | Capas data / domain / ui. El dominio no depende de Android. |
| UseCase / Interactor | Clase 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. |
| DataSource | RemoteDataSource (Retrofit), LocalDataSource (Room/DataStore). |
| DTO / Entity / Domain model | Tres 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. |
| Koin | DI alternativo, sin generación de código, resolución en runtime. Conocerlo de nombre. |
| Paging 3 | Listas paginadas con PagingSource / RemoteMediator. |
| WorkManager | Trabajo diferible y garantizado (sync, subir ficheros) que sobrevive a reinicios. |
| DataStore | Sustituto de SharedPreferences (≈ shared_preferences), asíncrono con Flow. Proto DataStore si quieres tipado. |
Equivalencias rápidas de librerías:
| Flutter | Android |
|---|---|
dio / http | Retrofit + OkHttp (interceptors ≈ interceptors de dio) |
json_serializable / freezed | kotlinx.serialization o Moshi |
sqflite / drift | Room |
flutter_bloc / riverpod | ViewModel + StateFlow |
go_router | Navigation Compose (type-safe con @Serializable) |
get_it | Hilt |
cached_network_image | Coil |
flutter_secure_storage | EncryptedSharedPreferences (deprecado) / Keystore + Tink |
local_auth | BiometricPrompt |
firebase_crashlytics | Crashlytics (igual) |
mocktail | MockK (o fakes, preferidos por Google) |
integration_test | Espresso / Compose UI test / UI Automator |
golden tests | Paparazzi / Roborazzi (screenshot testing) |
very_good_analysis | Detekt + 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
FakeAccountRepositorycon unMutableStateFlowinterno antes que MockK.ViewModel:
MainDispatcherRuleque reemplazaDispatchers.Mainpor unTestDispatcher.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érmino | Significado |
|---|---|
| Recomposition / recompose | Compose vuelve a ejecutar un composable. "Esto recompone en cada frame." |
| Hoist the state | Subir el estado al llamador. |
| Collect | Suscribirse a un Flow. "Lo colecto con lifecycle." |
| Main-safe | Se puede llamar desde el hilo principal sin bloquear. |
| Leak | Algo retiene una Activity muerta. |
| ANR | La app no responde (main thread bloqueado). |
| Config change | Rotación, tema, idioma… recrea la Activity. |
| Process death | El sistema mató el proceso en background. |
| Edge-to-edge / insets | Dibujar detrás de barras del sistema y respetar WindowInsets. Obligatorio con targetSdk 35. |
| Cold start / warm start | Arranque con proceso nuevo / con proceso vivo. |
| Jank | Frames perdidos (igual que en Flutter). |
| Obfuscation / mapping file | R8 renombra clases; el mapping.txt traduce los stack traces. |
| Flavor | Variante de build. |
| Version catalog / TOML | libs.versions.toml. |
| Gradle sync | El IDE relee la configuración. "¿Has hecho sync?" |
| Clean build | ./gradlew clean. El "flutter clean" de toda la vida. |
| Nit | Comentario menor de review. |
| LGTM | Looks good to me. |
| Flaky test | Test que falla a veces (casi siempre: dispatchers o tiempo real). |
| Staged rollout / halt rollout | Publicación gradual en Play / pararla. |
| Feature flag / remote config | Activar funcionalidades sin release. |
| KMP | Kotlin Multiplatform: compartir dominio/datos con iOS. Pregunta probable viniendo de Flutter. |
9. Frases: acento Flutter → acento nativo
| Lo que dirías viniendo de Flutter | Lo 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.