Aprende Kotlin Multiplatform sobre un proyecto real
No necesitas saber programar: cada concepto se explica desde cero, con ejemplos y ejercicios. Empiezas por lo básico, con variables y funciones en Kotlin y en Swift, construyendo Marcador, una app de fútbol amateur para Android y para iOS. Después escribes una Pokédex que comparte su lógica entre las dos plataformas con Kotlin Multiplatform.
0 de 313 lecciones
El temario
De cero a producción · 313 de 313 lecciones listas
Antes de empezar
Qué vas a construir, a quién va dirigida la guía y cómo se lee cada lección.
Tu Mac, listo
Xcode, Android Studio y el JDK, instalados y verificados antes de escribir una línea.
- 1Instalar Xcode y el simuladorDejar Xcode, las Command Line Tools y un simulador de iPhone listos para programar para iOS.25 min
- 2Instalar Android Studio y el SDKTener Android Studio, el SDK de Android, ANDROID_HOME y un emulador arrancado.40 min
- 3JDK y verificación del entornoInstalar el JDK 17, entender cómo se relaciona con el del IDE y con Gradle, y pasar el checklist que confirma que todo está en su sitio.18 min
Kotlin: el lenguaje
Escribes la lógica del torneo de Marcador en consola: variables, nulabilidad, clases, colecciones y errores.
- 4Tu primer proyecto en KotlinCrear el proyecto Marcador y el módulo :torneo, recorrer la vista Android, entender módulo frente a carpeta y ejecutar tu primer main.40 min
- 5Variables, tipos y operacionesGuardar datos en variables, conocer los tipos básicos y hacer cuentas con operadores y plantillas de texto.25 min
- 6Los ficheros de Gradle de tu proyectoConocer el mapa de ficheros de Gradle, entender el catálogo de versiones y el bloque dependencies, y instalar tu primera librería (kotlin-test) con la receta.35 min
- 7Funciones y parámetrosEscribir funciones con parámetros, valor de retorno y argumentos por nombre, y la diferencia entre devolver e imprimir.30 min
- 8Condiciones: if y whenDecidir qué hace el programa con if, else y when, y combinar condiciones con && y ||.30 min
- 9Bucles y tu primer testRepetir con rangos y bucles, y escribir tu primer test con kotlin.test.35 min
- 10NulabilidadUsar ?, ?., ?:, !!, let y smart casts sin miedo, y explicar por qué Kotlin evita el error del billón de dólares.35 min
- 11Clases y objetosEntender qué es una clase y un objeto, y escribir una clase con propiedades y métodos.35 min
- 12Clases que se validanImpedir que un objeto exista mal formado con init y require, y escribir Equipo, una clase que valida su plantilla al construirse.32 min
- 13Propiedades y visibilidadControlar cómo se lee y se cambia un dato de una clase con setters, private set y propiedades calculadas, y decidir quién puede usarla con la visibilidad y open o final.37 min
- 14data class e igualdadEntender data class, copy y la diferencia entre == y ===, y explicarlo en una entrevista.38 min
- 15Enum y when exhaustivoModelar un conjunto cerrado de valores con enum y leerlo con when, y dejar que el compilador compruebe que no falta ningún caso.25 min
- 16Sealed: estados con datosModelar casos distintos que llevan datos con sealed class y sealed interface, y escribir los eventos de un partido.35 min
- 17El partido hecho de eventosReconstruir Partido a partir de sus eventos y calcular el resultado con un when exhaustivo.40 min
- 18Lambdas y funciones de orden superiorEscribir lambdas, pasar funciones como parámetros y leer una trailing lambda sin dudar.30 min
- 19Lambdas con listas: filter, map y reglasUsar lambdas con filter, filterIsInstance y map, y guardar reglas como funciones en el torneo.32 min
- 20ColeccionesElegir entre List, Set y Map, y transformar colecciones con operaciones como map, filter, groupBy, fold y reduce.33 min
- 21Secuencias y formato de tablasProcesar listas con secuencias perezosas, contar con groupingBy y alinear columnas con format.25 min
- 22La tabla de posicionesConstruir la tabla de posiciones del torneo con flatMap, groupBy, comparadores y sortedWith.38 min
- 23Inmutabilidad y referenciasEntender por qué una val con una lista mutable dentro no es inmutable, y arreglarlo con copias defensivas.35 min
- 24Herencia, interfaces y composiciónEscribir clases con herencia, interfaces y clases abstractas, y elegir composición antes que herencia.40 min
- 25object, companion y varargCrear un único objeto con object, funciones de la clase con companion object, objetos anónimos y funciones con vararg.30 min
- 26ExtensionesAñadir funciones a tipos que no son tuyos con funciones y propiedades de extensión.40 min
- 27Funciones de alcanceElegir entre let, run, apply, also y with para abrir un bloque sobre un objeto.35 min
- 28GenéricosEscribir clases y funciones que valen para varios tipos con parámetros de tipo, incluidos dos tipos a la vez y extensiones genéricas.20 min
- 29Comparadores, Iterable y el rankingOrdenar con comparadores, hacer una clase recorrible con for y escribir el ranking del torneo.38 min
- 30Excepciones: try, catch y requireProvocar y capturar errores con throw, try, catch y finally, y comprobar condiciones con require, check y error.25 min
- 31Result: errores como valoresDevolver un error como un valor con Result y decidir entre excepción y Result.35 min
- 32Ficheros y depuradorLeer y escribir ficheros con use y depurar con breakpoints y la pila de llamadas.37 min
- 33Delegación: by lazyUsar by lazy para calcular un valor solo la primera vez que se lee.25 min
- 34JSON e instalar kotlinx.serializationEntender qué es JSON y instalar el plugin de serialización y la librería kotlinx.serialization paso a paso.35 min
- 35Guardar y leer JSON con JsonConvertir objetos a JSON y de vuelta con la clase Json, y entender por qué se guardan clases aparte.28 min
- 36Guardado.kt en MarcadorEscribir Guardado.kt: convertir el torneo a clases guardadas y de vuelta, y guardarlo en torneo.json.35 min
Corrutinas y Flow
El partido en vivo en consola: suspend, scopes, cancelación, Flow, StateFlow y tests con tiempo virtual.
- 37Corrutinas y suspendEntender qué es una corrutina y qué significa suspend, e instalar la librería de corrutinas.30 min
- 38El partido, minuto a minutoSimular un partido minuto a minuto con una función suspend y delay, y comprobarlo con un test.35 min
- 39launch y asyncLanzar trabajo concurrente con launch y esperar resultados con async y await.40 min
- 40Alcance, Job y cancelaciónEntender el alcance, el Job y la cancelación cooperativa de una corrutina.38 min
- 41Un alcance propioCrear un alcance propio con su contexto, entender GlobalScope y cancelar el trabajo de un Transmisor.38 min
- 42Dispatchers y withContextElegir entre Main, IO y Default y cambiar de hilo con withContext.40 min
- 43Errores en corrutinasManejar errores con try/catch, CoroutineExceptionHandler, SupervisorJob y supervisorScope.40 min
- 44Flow fríoEscribir un flow, recogerlo con collect y usar map, filter y flowOn.42 min
- 45Flow: scan y combineAcumular valores con scan y combinar varios flujos con combine.30 min
- 46flatMapLatest y debounceCambiar de flujo con flatMapLatest y esperar a que el usuario deje de escribir con debounce.30 min
- 47StateFlow y SharedFlowEntender StateFlow y SharedFlow, y cuándo usar cada uno.30 min
- 48stateIn, shareIn y los flujos calientesConvertir un flujo frío en caliente con stateIn y shareIn y comprobarlo con la demo de Jornada.30 min
- 49Tests de corrutinas: tiempo virtualProbar corrutinas con runTest, tiempo virtual e inyectando el dispatcher.33 min
- 50Tests con backgroundScope y TurbineProbar flujos con backgroundScope, el dispatcher Unconfined e instalar Turbine.35 min
Android: la plataforma
La app de Marcador en Android: la Activity, la rotación, el hilo principal, el Context, los permisos y Gradle.
- 51La ActivityHacer que :app dependa de :torneo y ejecutar Marcador por primera vez en el emulador, con la tabla de :torneo en pantalla.35 min
- 52El ciclo de vida de la ActivityVer en Logcat cada callback de la Activity al arrancar, ir a Home y volver, y rotar, y saber por qué onDestroy no significa que el usuario se fue.38 min
- 53Rotación y muerte del procesoVer qué se pierde al rotar y al matar el proceso, y guardar estado con onSaveInstanceState.40 min
- 54El hilo principal y el ANREntender el hilo principal y el ANR, e instalar las corrutinas en :app para sacar los cálculos pesados del hilo principal.38 min
- 55Lifecycle y repeatOnLifecycleRecoger el partido en vivo solo mientras la pantalla se ve, con Lifecycle y repeatOnLifecycle.35 min
- 56Context y ApplicationEntender qué es un Context, la clase Application, e instalar LeakCanary.35 min
- 57Fugas de memoriaReproducir una fuga de memoria con un Context, leer el informe de LeakCanary y arreglarla.40 min
- 58Intents explícitos y una segunda pantallaAbrir otra pantalla de tu app con un intent explícito, pasarle datos con extras y cerrar con finish.35 min
- 59Intents implícitos y filtrosPedirle al sistema que abra un enlace o comparta un texto con un intent implícito, y recibir enlaces con un filtro.30 min
- 60PermisosPedir un permiso en tiempo de ejecución y reaccionar a la respuesta del usuario.38 min
- 61NotificacionesMostrar un aviso de gol con un canal de notificaciones, sin pisar los anteriores.40 min
- 62Trabajo en segundo planoEntender qué trabajo puede hacerse en segundo plano en Android e instalar WorkManager.25 min
- 63Un recordatorio con WorkManagerProgramar un recordatorio diario con un Worker de WorkManager y comprobar que sobrevive a matar el proceso.40 min
- 64Gradle: ficheros, catálogo y dependenciasDistinguir implementation de api, entender namespace frente a applicationId y cómo elige Android los recursos.25 min
- 65Gradle: build types y BuildConfigEntender los build types, el bundle de dependencias y BuildConfig.35 min
- 66Gradle: flavors y variantesCrear dos flavors (Gratis y Pro) con sus carpetas y recursos, y compilar las cuatro variantes.35 min
Jetpack Compose
Las pantallas de Marcador: estado, state hoisting, recomposición y efectos, sobre pantallas reales.
- 67Compose: composables y modifiersEscribir tus primeros composables y colocarlos con Row, Column, Box y Modifier, y verlos en la vista previa.40 min
- 68Compose: Scaffold, slots y temaMontar la pantalla del Marcador con Scaffold, slots y el tema de la app, y verla en el emulador.38 min
- 69Estado en ComposeDistinguir remember, mutableStateOf y rememberSaveable: qué sobrevive a qué.40 min
- 70State hoistingSeparar composables con estado y sin estado y subir el estado al sitio correcto.40 min
- 71Campos de texto: TextFieldEscribir un campo de texto con su estado en la pantalla y que lo escrito sobreviva a girar el móvil.25 min
- 72Listas con LazyColumnMostrar listas largas con LazyColumn, claves estables y vistas previas con datos de ejemplo.35 min
- 73Pestañas con NavigationBarNavegar entre pantallas con NavigationBar, ajustar los insets y reproducir el error de la clave repetida.38 min
- 74RecomposiciónEntender qué dispara una recomposición, ver una fila que se recompone de más y arreglarla.38 min
- 75Efectos en Compose: LaunchedEffectEntender qué es un efecto secundario y lanzar trabajo atado a un composable con LaunchedEffect y rememberUpdatedState.38 min
- 76Más efectos: scope, derivedStateOf y SideEffectLanzar trabajo desde un botón con rememberCoroutineScope y derivar estado con derivedStateOf y SideEffect.38 min
- 77Efectos y ciclo de vida: DisposableEffectRegistrar y deshacer un efecto atado al ciclo de vida de la pantalla con DisposableEffect, e instalar lifecycle-runtime-compose.36 min
- 78Recoger un Flow con el ciclo de vidaUsar collectAsStateWithLifecycle y ver cómo un partido guardado en remember se reinicia al rotar.38 min
Jetpack y arquitectura
ViewModel, UiState, capas, Room, DataStore y navegación: la arquitectura que recomienda Google.
- 79ViewModel: qué sobreviveSaber qué sobrevive un ViewModel, usar viewModelScope y por qué no GlobalScope.30 min
- 80ViewModel en Marcador: el partidoLlevar el partido de Marcador a un ViewModel y comprobar que sobrevive a girar la pantalla.35 min
- 81SavedStateHandleConservar estado a través de la muerte del proceso con SavedStateHandle.38 min
- 82Modelar el UiStateModelar la pantalla con un UiState de carga, éxito y error.30 min
- 83UiState en Marcador: el ViewModelMover el estado de la pantalla a un UiState dentro del ViewModel de Marcador.30 min
- 84Capas y flujo unidireccionalOrganizar la app en capas de UI, dominio y datos con una única fuente de la verdad.30 min
- 85Capas en Marcador: el directoMover el directo del partido a su propia capa con un único dueño del reloj.30 min
- 86Repository: una sola puerta a los datosEscribir un Repository y un caso de uso.40 min
- 87Casos de uso: RegistrarGolAñadir un caso de uso que registra un gol sobre el repositorio y comprobar el 3-2 → 4-2.38 min
- 88Hilt: instalarlo y el primer móduloEntender qué es inyectar una dependencia y por qué hace falta alguien que monte las piezas; instalar Hilt y KSP en Marcador y escribir el primer módulo.40 min
- 89Hilt y los ViewModelsQue Hilt construya los ViewModels de Marcador con @HiltViewModel y hiltViewModel(), sin fábricas a mano.35 min
- 90Room: entidades y DAOInstalar Room y guardar los partidos: entidades, DAO y consultas con Flow.38 min
- 91Hilt abre la base de datosQue Hilt construya la base de datos de Room y entregue el DAO: @ApplicationContext, una receta con Context y el primer cualificador.35 min
- 92El repositorio con RoomQue PartidosRepositoryRoom traduzca la base al dominio y que Hilt lo entregue en lugar de la lista en memoria.40 min
- 93Room: migrar la base de datosCambiar el esquema de una base de datos que ya tiene datos sin perderlos, con una migración automática.38 min
- 94DataStoreGuardar preferencias con DataStore y compararlo con SharedPreferences.30 min
- 95DataStore en Marcador: la duración del partidoGuardar la duración del partido con DataStore y usarla en el directo de Marcador.35 min
- 96Navigation 3: la pila de pantallasNavegar de lista a partido y a goleadores con Navigation 3.35 min
- 97Navegación en MarcadorNavegar en Marcador entre la lista, el partido y los goleadores con Navigation 3 (el partido abre aún siempre el 1).38 min
- 98El id del partido: inyección asistidaQue un ViewModel reciba a la vez lo que Hilt sabe construir y un dato que solo se conoce al navegar, con @AssistedInject y hiltViewModel(creationCallback).35 min
- 99Tests de un ViewModel: fakes, setMain y TurbineProbar un ViewModel con un fake, runTest y Turbine.35 min
- 100Tests del ViewModel de MarcadorEscribir los tests del ViewModel de Marcador con fakes, setMain y Turbine.40 min
- 101Red: HTTP y JSONEntender qué es una petición HTTP, su respuesta y el JSON, y separar el DTO del modelo.30 min
- 102Retrofit y la primera llamadaInstalar Retrofit y OkHttp, hacer la primera llamada real y dejar en Marcador la interfaz de la API.35 min
- 103Red en Marcador: la lista de árbitrosCargar los árbitros desde internet y pintarlos con los estados Cargando, Éxito y Error.45 min
- 104POST y errores de redEnviar datos con un POST y convertir los fallos de red en errores que la pantalla entienda.40 min
- 105La pantalla de la crónica y sus pruebasPublicar la crónica desde una pantalla y comprobar con tests los errores de red.40 min
- 106Dos fuentes: el almacén y el repositorioGuardar lo que llega de la red en Room y construir el repositorio donde Room manda y la red refresca.40 min
- 107Dos fuentes: la pantalla y las pruebasPintar siempre lo guardado, avisar cuando la red falla y probar el repositorio de dos fuentes.40 min
- 108Casos de uso: cuándo sí y cuándo sobranDecidir cuándo un caso de uso aporta y cuándo sobra, y conectarlo con dos repositorios y el ViewModel.30 min
- 109Inyección en todas las capasOrdenar los módulos de Hilt por capas (red, datos, repositorios), entender los alcances y ver el mismo grafo con Koin.35 min
- 110La plantilla, guardada en RoomGuardar los jugadores de cada equipo en una tabla nueva de Room y migrar la base de la versión 3 a la 4.45 min
- 111Una fila que se toca: la plantillaAbrir la plantilla de un equipo al tocar su fila en Tablas.30 min
- 112Formularios: el estado y las reglasGuardar el estado del formulario en el ViewModel con campos tocados y calcular los errores como un tipo, con pruebas.48 min
- 113Formularios: la pantalla, el teclado y las pruebasMontar el formulario de añadir jugador con errores bajo cada campo, teclado numérico, foco y un botón Guardar.45 min
- 114La forma más nueva: TextFieldState y SecureTextFieldConocer TextFieldState y SecureTextField, la forma más nueva de guardar el texto de un campo y de ocultar una contraseña.40 min
- 115Estado de toda la app: el partido en juegoEntender qué es el estado de toda la app y guardar el partido en juego en una sola pieza que viva en toda la app.40 min
- 116El partido sigue aunque salgasHacer que el partido siga corriendo al salir de su pantalla y que solo haya uno en juego a la vez.40 min
- 117Solo un partido a la vezImpedir que haya dos partidos en juego: el aviso en el otro partido, los chips de duración y el botón para ir al que se juega.35 min
- 118La barra del partido en juegoMostrar en todas las pantallas una barra con el marcador en vivo, que lleva al partido y se cierra al terminar.40 min
Swift: el lenguaje
La misma lógica del torneo, reescrita en Swift y comparada línea a línea con tu versión de Kotlin.
- 119Tu primer paquete de SwiftCrear el Swift Package Torneo, escribir let y var, y correr tu primer test con Swift Testing.38 min
- 120La regla de los puntos y tu primer testEscribir la regla de los puntos en Swift y tu primer test, hasta llegar a 4 tests.38 min
- 121OpcionalesUsar ?, !, if let, guard let, ?? y optional chaining, y compararlos con la nulabilidad de Kotlin.30 min
- 122guard let y textos que no son númerosUsar guard let y convertir textos en números sin que la app se rompa.28 min
- 123Funciones y switchEscribir funciones con etiquetas de argumento y valores por defecto, y usar switch exhaustivo.35 min
- 124Valores por defecto e inoutDar valores por defecto a los parámetros y modificar un valor con inout.20 min
- 125struct frente a classDistinguir tipos de valor y de referencia, copy-on-write y mutating.45 min
- 126mutating y copias: Partido y TorneoModelar Partido y Torneo con structs mutating y entender las copias.40 min
- 127Enums con valores asociadosModelar eventos con enums con valores asociados y raw values.35 min
- 128Enums: el partido de eventosResolver el partido a partir de una lista de eventos con enums.35 min
- 129ClosuresEscribir closures, trailing closures, capturas y @escaping.40 min
- 130Closures con listasUsar closures con listas: map, filter y ordenar.30 min
- 131Protocolos y extensionesEscribir protocolos, extensiones e implementaciones por defecto, y pensar en protocolos.40 min
- 132Equatable y HashableHacer que tus tipos se puedan comparar y usar como claves con Equatable y Hashable.28 min
- 133Genéricos en SwiftEscribir genéricos, entender los associated types y distinguir some de any.40 min
- 134some y anyEntender some y any con protocolos genéricos.45 min
- 135Errores: throws, try y do/catchLanzar y capturar errores con throws, do/try/catch, try? y Result.38 min
- 136registrar lanza errores: throws tipado y ResultHacer que registrar lance errores con throws tipado y Result.40 min
- 137Colecciones y key pathsUsar array, set y diccionario, recorrerlos, transformarlos y ordenarlos con key paths.45 min
- 138Propiedades: calculadas, didSet y lazyUsar propiedades calculadas, didSet y lazy.35 min
- 139Codable: el enum con tipoLeer con Codable el JSON que escribió tu versión de Kotlin.30 min
- 140Codable: guardar y cargar el torneoConvertir el torneo a JSON, escribirlo en un fichero y volver a leerlo.30 min
- 141El JSON de Kotlin en Swift: recursos y errores de lecturaLeer en Swift el JSON que guardó Kotlin y manejar los errores de lectura.40 min
Swift: memoria y concurrencia
ARC, async/await, actores y un retain cycle a propósito que detectarás y arreglarás.
- 142ARCEntender cómo ARC cuenta referencias, y qué son strong, weak y unowned.35 min
- 143unowned y tests de memoriaUsar unowned cuando la otra referencia vive más y comprobar la memoria con tests.28 min
- 144async/await y TaskEscribir funciones async, lanzar Task y cancelar.37 min
- 145Cancelar una tareaCancelar una Task y hacer que el código lo respete.33 min
- 146async let: varias tareas a la vezLanzar varias tareas a la vez con async let y TaskGroup.36 min
- 147TaskGroup y errores en tareas hijasLanzar un número variable de tareas con TaskGroup y manejar sus errores.40 min
- 148AsyncSequence y AsyncStreamProducir eventos con AsyncStream, el equivalente de Flow.40 min
- 149El marcador gol a golEmitir los goles de un partido como AsyncStream y recorrerlos.35 min
- 150ActoresProteger estado compartido con un actor y entender la reentrancia.35 min
- 151Reentrancia: la trampa de los actoresEntender la reentrancia de un actor y evitar sus carreras lógicas.30 min
- 152@MainActor y el hilo principalAislar el código de pantalla al hilo principal con @MainActor y entender qué cambia en un proyecto de Xcode 26.30 min
- 153Sendable y el modo Swift 6Cambiar el modo de lenguaje del paquete a Swift 6, marcar los tipos como Sendable y escribir el estado de pantalla en el actor principal, sin avisos.40 min
- 154El estado de pantalla en el actor principalMantener el estado de una pantalla en el actor principal.30 min
- 155Retain cyclesProvocar un ciclo de retención con un closure, detectarlo con leaks y arreglarlo con [weak self].30 min
- 156Delegados y tareasEvitar ciclos con delegados weak y con Task que capturan self.30 min
iOS con SwiftUI
Tu primera app de iOS y SwiftUI desde cero: vistas, estado, listas, identidad y tareas, con las mismas pantallas que en Android.
- 157Tu primera app de iOSCrear la app de iOS, enlazar el paquete Torneo, entender App, Scene y scenePhase y escribir tus primeras pruebas.35 min
- 158La app, sus escenas y el paquete TorneoEntender App, Scene y scenePhase y enlazar el paquete Torneo a la app de iOS.35 min
- 159Tus primeras pruebas en iOSEscribir tus primeras pruebas unitarias y de interfaz de la app de iOS.37 min
- 160Un UIViewController en SwiftUIVer cada callback de un UIViewController y compararlo con el de la Activity.28 min
- 161El ciclo de vida de UIViewControllerVer cada callback de un UIViewController y compararlo con el de la Activity.38 min
- 162SwiftUI: primeros pasosEscribir vistas con struct View, body, modifiers y stacks.40 min
- 163El orden de los modificadoresVer por qué el orden de los modifiers cambia lo que se pinta.30 min
- 164@StateUsar @State para que la vista se redibuje cuando cambia un valor, y compararlo con remember de Compose.27 min
- 165@BindingUsar @Binding y compararlo con el state hoisting de Compose.35 min
- 166Campos de texto en SwiftUIEscribir un campo de texto con @State y un Binding, y evitar el error de pasar el valor en vez del enlace.25 min
- 167Listas en SwiftUIPintar listas con List, ForEach e Identifiable, y entender la identidad de las vistas.35 min
- 168La identidad de una vistaEntender qué es la identidad de una vista y por qué importa en las listas.30 min
- 169ObservableObject y @StateObjectDistinguir @StateObject de @ObservedObject y de @EnvironmentObject, con el bug que reinicia el partido.38 min
- 170@ObservedObject y el partido que se reiniciaVer qué pasa con @ObservedObject cuando la vista se recrea.40 min
- 171@EnvironmentObject: los ajustes para toda la appCompartir los ajustes con toda la app mediante @EnvironmentObject.33 min
- 172@ObservableMigrar a @Observable con @Bindable y @Environment.40 min
- 173.task frente a .onAppearLanzar trabajo asíncrono ligado a la vista con .task, que se cancela solo.29 min
- 174El partido arranca con .task(id:)Arrancar el partido con .task(id:) y probarlo.30 min
Arquitectura en iOS
MVVM, navegación, persistencia, pruebas y red con URLSession: dos fuentes de datos, casos de uso e inyección en todas las capas. La arquitectura de Marcador en iOS, la misma que en Android. Aquí van también los permisos y las notificaciones: en iOS dependen del ViewModel.
- 175MVVM: el estado, el repositorio y el caso de usoEscribir un ViewModel con @MainActor y un caso de uso, con el ciclo del cronómetro ya evitado con [weak self].33 min
- 176El ViewModel y el hilo principalEscribir el ViewModel y entender el hilo principal.36 min
- 177La pantalla pinta el uiStateHacer que la pantalla pinte el uiState del ViewModel.34 min
- 178Ver un ciclo de retención: deinit y Memory GraphVer con deinit y con el Memory Graph Debugger que un ViewModel no se libera, y reconocer el ciclo.32 min
- 179NavigationStackNavegar con NavigationStack y navegación por valor.30 min
- 180Navegar a los goleadoresNavegar a la pantalla de goleadores y volver.38 min
- 181Ajustes que sobreviven: UserDefaults y @AppStorageGuardar preferencias con @AppStorage y el torneo en un fichero de Documents.31 min
- 182Guardar el torneo en un ficheroGuardar y cargar el torneo en ficheros.40 min
- 183Permisos en iOSDeclarar permisos en Info.plist y mostrar una notificación.35 min
- 184Notificaciones localesProgramar y mostrar notificaciones locales.38 min
- 185Tests en iOS: falsos y espíasProbar el ViewModel con XCTest y Swift Testing.32 min
- 186XCTest y las pruebas del ViewModelProbar el ViewModel con XCTest.35 min
- 187Red: HTTP, JSON y URLSessionEntender una petición HTTP y su respuesta y hacer el primer GET con URLSession, async/await y Codable.40 min
- 188Red en Marcador: la lista de árbitrosCargar los árbitros desde internet y pintarlos con los estados de carga, éxito y error.42 min
- 189POST y errores de redEnviar datos con un POST y convertir los fallos de red en errores que la pantalla entienda.38 min
- 190La pantalla de la crónica y sus pruebasPublicar la crónica desde una pantalla y comprobar con pruebas los errores de red.40 min
- 191Dos fuentes: el almacén y el repositorioGuardar lo que llega de la red en disco y construir el repositorio donde el disco manda y la red refresca.38 min
- 192Dos fuentes: la pantalla y las pruebasPintar siempre lo guardado, avisar cuando la red falla, probar el repositorio y mirarlo funcionar.35 min
- 193Casos de uso: cuándo sí y cuándo sobranDecidir cuándo un caso de uso aporta y cuándo sobra, y conectarlo con dos repositorios y el ViewModel.35 min
- 194Inyección de dependencias en todas las capasMontar el contenedor de la app por capas y hacerlo llegar a las vistas por @Environment.38 min
- 195Equipos y plantillas en iOSVer los equipos y su plantilla en pantalla y guardar un jugador nuevo en el fichero del torneo, sin romper los partidos.48 min
- 196Formularios en SwiftUI: el estado y las reglasGuardar el estado del formulario en un struct con campos tocados y calcular los errores como un tipo, con pruebas.47 min
- 197Formularios en SwiftUI: Form, foco y guardarMontar el formulario con Form, teclado numérico, foco y un botón Guardar que solo se activa si todo es válido.50 min
- 198El directo, en su propia claseSacar el reloj y los goles del partido a su propia clase, sin cambiar lo que hace la app.38 min
- 199Estado de toda la app en SwiftUIGuardar el partido en juego en un objeto de toda la app, repartido con el entorno de SwiftUI.38 min
- 200El partido sigue aunque salgas, en iOSHacer que el partido siga corriendo al salir de su pantalla y que solo haya uno en juego a la vez.40 min
- 201Solo un partido a la vez, en iOSImpedir que haya dos partidos en juego: el aviso en el otro partido y el selector de duración de cada partido.35 min
- 202La barra del partido en juego, en iOSMostrar en todas las pestañas una barra con el marcador en vivo que lleva al partido.45 min
- 203La barra abre el partidoSubir la pestaña y la pila de navegación a la raíz para que tocar la barra abra el partido, y probarlo.35 min
- 204Escribiste la misma lógica dos vecesVer cuánto código se duplicó entre Kotlin y Swift y qué pregunta responde el curso de la Pokédex.25 min
Entorno y primer proyecto
Del Mac recién estrenado al proyecto corriendo en el emulador y en el simulador.
- 205Qué es KMP y qué noDistinguir KMP de Compose Multiplatform y decidir cuánto código vas a compartir.27 min
- 206Crear el proyecto PokédexInstalar el plugin de Kotlin Multiplatform, generar el proyecto en ~/Pokedex-KMP y llevarlo al stack exacto que usa la guía.36 min
- 207El módulo compartido y la prueba de humoDejar el módulo compartido con su saludo y su primera prueba, y ejecutarla en los tres destinos.40 min
- 208Ejecutar en AndroidEjecutar la app de un proyecto multiplataforma en Android y seguir el camino del saludo desde commonMain hasta la pantalla.20 min
- 209Ejecutar en iOSCompilar el framework de Kotlin y ver la app en el simulador desde Xcode.25 min
- 210Anatomía del proyectoLeer y navegar el proyecto generado: módulos, source sets, catálogo de versiones y la receta para instalar una librería.38 min
- 211Gradle sin dolorEntender qué tarea produce el framework de iOS y cómo diagnosticar un build que falla.30 min
- 212Integración con iOSInstalar a mano la conexión entre Xcode y tu módulo compartido, reconocer el error de cada pieza que falta y conocer los otros cuatro métodos.36 min
Cimientos del código compartido
Logs, interop, corrutinas y ViewModels. Cada lección deja algo visible en las dos pantallas.
- 213La primera dependenciaAñadir Kermit al código compartido y ver el mismo log en Logcat y en la consola de Xcode.38 min
- 214expect/actual frente a interfacesElegir bien entre expect/actual y una interfaz con implementaciones por plataforma.42 min
- 215Los tipos que cruzan a SwiftPredecir cómo llega cada tipo de Kotlin al Shared.h que después lee Swift.40 min
- 216Las funciones que cruzan a SwiftExponer funciones suspend y errores a Swift sin que una excepción mate el proceso.40 min
- 217Corrutinas en commonMainEscribir código asíncrono compartido con dispatchers inyectados y sin bloquear el hilo principal.40 min
- 218Un Flow hasta SwiftConsumir un Flow de Kotlin desde SwiftUI con KMP-NativeCoroutines.40 min
- 219Cancelar desde SwiftEntender los nombres que genera el plugin y cancelar bien un Flow de Kotlin desde Swift.28 min
- 220El ViewModel compartidoMontar las capas y el UiState de Google en commonMain y ver cómo se ve desde Swift.25 min
- 221El ViewModel compartido en AndroidConsumir el ViewModel compartido desde Compose en la app de Android.20 min
- 222El mismo ViewModel en iOSSostener un ViewModel de Kotlin desde SwiftUI con su ciclo de vida correcto y sin fugas.45 min
- 223Koin: un solo grafo en commonMainEntender Koin desde cero y montar un único grafo de dependencias en commonMain, con un test que lo verifica.40 min
- 224Arrancar Koin desde Android y desde iOSArrancar el grafo de Koin desde Android y desde iOS y pedir el ViewModel en cada interfaz.30 min
La lista de Pokémon
La primera pantalla entera: PokeAPI, tests del núcleo, paginación, errores con reintento, imágenes y las dos interfaces nativas.
- 225HTTP y Ktor: el cliente de PokeAPIEntender qué es HTTP y pedir la primera página de PokeAPI con Ktor desde código compartido.40 min
- 226Del JSON al Pokémon: DTO y repositorioConvertir el JSON de PokeAPI en un DTO y entregar al resto de la app el modelo de dominio desde un repositorio.40 min
- 227Testing del núcleo compartidoEscribir tests en commonTest que corran en JVM y en el simulador y atrapen bugs reales.40 min
- 228Paginación con Paging 3Servir la lista por páginas desde commonMain sin escribir el paginador a mano.33 min
- 229Probar la paginaciónComprobar con tests que la lista pide páginas cuando toca y para cuando se acaba.25 min
- 230Errores que se pueden contarConvertir un fallo de red en un mensaje útil que la interfaz pueda enseñar.29 min
- 231Reintentos con KtorReintentar de verdad cuando el servidor falla, con espera creciente y probado con tests.21 min
- 232El puente de paginación para iOSEnvolver PagingDataPresenter en un StateFlow que SwiftUI pueda consumir sin saltos.38 min
- 233Probar el puente en iOSProbar el presentador de la paginación en la JVM y en el simulador de iOS.22 min
- 234La lista en AndroidPintar en Compose la lista paginada con imágenes, tarjetas y estados.37 min
- 235Refresco, barra y accesibilidad en AndroidAñadir a la lista el gesto de refrescar, la barra que se encoge y la descripción para TalkBack.25 min
- 236La lista en iOSPintar la misma lista en SwiftUI con AsyncImage, prefetch y refresco.48 min
Detalle y navegación
Dos peticiones en paralelo y un mapa de pantallas que sirve para Navigation 3 y para NavigationStack.
- 237El detalle en el núcleoCombinar dos peticiones en paralelo y limpiar el texto que devuelve PokeAPI.35 min
- 238El ViewModel del detallePoner el estado de la ficha en un ViewModel compartido, registrarlo en Koin con parámetro y probarlo.30 min
- 239El mapa de pantallas compartidoModelar los destinos una sola vez y convertirlos en URL para las dos plataformas.35 min
- 240Navegación en Android con Navigation 3Ir de la lista al detalle y volver, con un ViewModel propio por entrada de la pila.25 min
- 241La pila de pantallas en AndroidGuardar la pila, abrir la ficha desde la lista, gestionar el atrás y ejecutar la navegación.25 min
- 242Navegación en iOS con NavigationStackAbrir la ficha desde la lista en SwiftUI con NavigationStack, con la pila de ids que ya conoce el núcleo compartido.35 min
Búsqueda
PokeAPI no tiene endpoint de búsqueda. Así se resuelve: índice local, debounce y una barra nativa en cada app.
- 243El índice de búsquedaTraer una sola vez los nombres de todos los Pokémon y guardarlos como índice local, con un candado y un test que cuenta las llamadas.28 min
- 244El ViewModel de la búsquedaFiltrar el índice con debounce y flatMapLatest, publicar el estado y probarlo con tiempo virtual.40 min
- 245La barra de búsqueda en AndroidPoner la barra de búsqueda de Material 3 sobre el estado compartido.35 min
- 246La búsqueda en iOS con .searchablePoner el campo de búsqueda nativo de SwiftUI sobre el mismo estado compartido.28 min
Favoritos con Room
Base de datos compartida, corazones en las dos apps, la migración que simula al usuario que ya tenía la app y la lista de Pokémon que sigue funcionando sin conexión.
- 247Persistencia con Room 3Montar la base de datos compartida: las librerías, la entidad, el DAO y la base.38 min
- 248La base en cada plataformaHacer que la base arranque igual en Android y en iOS, y ver qué pasa si falta una de las piezas de cada plataforma.28 min
- 249Favoritos en el núcleoGuardar y observar favoritos con un Flow que la lista pueda combinar sin recargarse entera.33 min
- 250Quitar con deshacerQuitar un favorito dejando un aviso para deshacerlo, como estado del ViewModel y no como evento, y probarlo con tests.27 min
- 251Favoritos en AndroidAñadir el corazón a la rejilla de la lista y que quede guardado entre sesiones.28 min
- 252Pestañas en AndroidAñadir una barra de navegación inferior y una pantalla de favoritos, sin romper la pila de Navigation 3.33 min
- 253Deslizar y deshacer en AndroidQuitar un favorito deslizando la fila y ofrecer «Deshacer» con un aviso que no se pierde al girar la pantalla.28 min
- 254Favoritos en iOSAñadir la barra de pestañas y el corazón consumiendo el mismo Flow de favoritos.40 min
- 255Migraciones de base de datosCambiar el esquema sin borrar los datos del usuario: la columna nueva, la versión y el dato que atraviesa el núcleo.38 min
- 256Probar la migraciónDemostrar con un test que la base migra sin perder los favoritos y ver qué pasa si falta cada pieza.33 min
- 257La base como fuente de verdadEntender por qué la pantalla lee solo de la base y la red solo la rellena, preparar las tablas de Pokémon y de claves y probar que la base, ya en la versión 3, conserva los favoritos.44 min
- 258El RemoteMediator: la red llena la baseEscribir el mediador que pide páginas a PokeAPI y las guarda en la base, y probarlo.38 min
- 259El repositorio con dos fuentesDecidir cuándo la copia caduca, montar el repositorio con el Pager que lee de la base y cambiar el ViewModel de la lista.30 min
- 260Los tests del repositorioProbar con tests que la copia caduca cuando toca y que el repositorio sirve lo guardado, incluido el test del presentador de iOS.36 min
- 261La lista sin red en AndroidEnseñar lo guardado cuando no hay conexión, con su aviso y su reintento, y comprobarlo en modo avión.42 min
- 262La lista sin red en iOSMostrar en SwiftUI el mismo aviso desde el estado compartido y probar el presentador.29 min
Ajustes con DataStore
Preferencias compartidas, el tema aplicado desde la raíz y un «Acerca de» que cita a quien toca.
- 263Ajustes en el núcleoInstalar DataStore y crear el almacén de preferencias en commonMain, con una ruta por plataforma.26 min
- 264El repositorio y el ViewModel de los ajustesLeer y guardar las preferencias desde un repositorio, exponerlas al ViewModel y probarlas con cinco tests.28 min
- 265Ajustes y favoritos para toda la appCombinar el orden de los ajustes con la lista de favoritos, nombrar el estado de toda la app y calcular el número de la insignia de favoritos.22 min
- 266Ajustes en AndroidMontar la pantalla de ajustes en Android: la pestaña, la fila reutilizable y la hoja para elegir el tema.30 min
- 267Los ajustes mandan en toda la app, en AndroidAplicar el tema desde la raíz y mostrar el número de favoritos en la pestaña con BadgedBox.22 min
- 268Ajustes en iOSMontar la misma pantalla con Form, aplicar el tema con preferredColorScheme y mostrar el número de favoritos con badge.35 min
Deep links y compartir
Que un enlace abra el Pokémon correcto: con la app cerrada, con la app abierta y desde otro móvil.
- 269Deep links en AndroidAbrir el Pokémon correcto desde un enlace, con la app cerrada y con la app abierta.45 min
- 270Deep links en iOSRecibir el mismo enlace en SwiftUI y saber qué cuesta pasar a Universal Links.45 min
- 271Compartir un PokémonGenerar una URL que funcione fuera de la app y ofrecerla con el diálogo nativo de cada sistema.28 min
El Pokémon del día
Una notificación local diaria en las dos plataformas, sin servidor y sin push.
- 272El Pokémon del día en el núcleoElegir el Pokémon del día de forma determinista y modelar el permiso de notificaciones.42 min
- 273Notificaciones en Android: el canal y el avisoDeclarar el permiso, crear el canal y publicar una notificación que abre la ficha.30 min
- 274El permiso de notificaciones en AndroidPedir el permiso en Android 13 o posterior, leer sus tres estados y llevar al usuario a los ajustes si dijo que no.30 min
- 275Trabajo en segundo plano en AndroidProgramar el aviso diario con WorkManager y probarlo sin esperar veinticuatro horas.34 min
- 276Notificaciones en iOSProgramar una semana de avisos locales con contenido distinto y reponerlos al volver a la app.47 min
- 277Del aviso a la ficha, en iOSAbrir la ficha del Pokémon al tocar el aviso, con la app cerrada o abierta.32 min
Autenticación con Supabase
Registro, login y refresco de tokens hablando REST a pelo, hasta que ninguna pantalla se vea sin sesión.
- 278Preparar SupabaseTener un proyecto de Supabase con email y contraseña, probado con curl antes de escribir Kotlin.25 min
- 279Configuración y secretos por entornoLlevar la URL y la clave hasta las dos apps sin escribirlas en el código.38 min
- 280Autenticación en el núcleo: la APIRegistrar, entrar y salir contra la API REST de Supabase, con la respuesta traducida a tipos de Kotlin.28 min
- 281La sesión en el núcleoGuardar el estado de sesión en un StateFlow que lea toda la app.27 min
- 282Refrescar el token sin duplicadosRenovar la sesión con el plugin Auth de Ktor y sobrevivir a dos peticiones concurrentes.28 min
- 283El cliente que lleva tu sesiónPoner el plugin Auth de Ktor, el interceptor del 403 y sus pruebas.30 min
- 284Guardar los tokensGuardar la sesión en el sandbox de Android y en el Llavero de iOS, con Swift implementando una interfaz de Kotlin.38 min
- 285El formulario de entrada en AndroidEscribir el formulario de entrada con campos tocados y errores calculados.30 min
- 286La puerta y la cuenta en AndroidDecidir desde la raíz qué ve el usuario según su estado de sesión.25 min
- 287El formulario de entrada en iOSEscribir en SwiftUI el formulario de entrada, con los mismos errores por campo.28 min
- 288La puerta y la cuenta en iOSMontar la misma raíz en SwiftUI y aplicar el deep link pendiente después del login.25 min
- 289Recuperar la contraseñaPedir el correo de recuperación y dejar la app lista para recibir su enlace.25 min
- 290El enlace del correo y la contraseña nuevaAbrir el enlace del correo, que no es una pantalla, y cambiar la contraseña desde dentro.35 min
- 291Eliminar la cuentaEliminar la cuenta con una función de la base de datos y un botón en las dos apps (lo que exigen las tiendas), y comprobar que ya no puedes entrar.33 min
Calidad
Tests de interfaz en las dos plataformas, depuración a través del puente, accesibilidad y dos idiomas.
- 292Pruebas de interfaz en AndroidProbar la lista con datos de mentira, sin emulador.25 min
- 293Probar la pantalla de entrada en AndroidProbar la pantalla de sesión separando lo que se ve de lo que se decide.27 min
- 294Pruebas de interfaz en iOSCrear el target de pruebas en Xcode y automatizar un flujo con XCUITest.33 min
- 295Medir: cuánto ocupa la appMedir cuánto ocupa la app en Android y en iOS con una tarea de Gradle.20 min
- 296El fallo que cruza a SwiftEncontrar el problema cuando el error viene de Kotlin pero explota en Swift.25 min
- 297Accesibilidad: la letra grandeQue la app aguante la letra grande en las dos plataformas.22 min
- 298Accesibilidad: el lector de pantallaRecorrer la app con TalkBack y VoiceOver y arreglar lo que no se puede usar.28 min
- 299Dos idiomas: el idioma en el núcleoQue el módulo compartido pida a PokeAPI los textos en el idioma del usuario, con una lista de preferencias y tests que fijan el idioma.20 min
- 300Dos idiomas en Android: los recursosPasar los textos de Android a recursos y traducirlos al inglés.30 min
- 301Dos idiomas en Android: todas las pantallasTraducir el resto de las pantallas de Android y comprobar cada idioma con tests.30 min
- 302Dos idiomas en iOSCrear el catálogo de cadenas, traducir la app de iPhone y arreglar los sitios donde SwiftUI no traduce solo.25 min
Producción
Del build de debug al binario firmado y al formulario de cada tienda. Sin subir nada.
- 303Producción en AndroidFirmar, ofuscar y probar la release en un dispositivo antes de que la pruebe un usuario.40 min
- 304Producción en iOSArchivar la app con su manifiesto de privacidad y su versión sacada de un único sitio.40 min
- 305Checklist de las dos tiendasSaber exactamente qué pide Play Console y qué pide App Store Connect antes de darle a publicar.15 min
Apéndice: más allá de la app
Migrar un módulo de Marcador a KMP, publicar la librería, la memoria de Native y compartir también la UI.
- 306Migrar un proyecto Android existenteMigrar :torneo, el módulo de Marcador, de módulo JVM a Kotlin Multiplatform con destinos JVM, Android e iOS, destino a destino y con el compilador como guía.110 min
- 307Migrar el disco con expect/actualDevolver a commonMain las funciones de disco de :torneo con dos declaraciones expect y un actual por plataforma.60 min
- 308Publicar una librería KMPPublicar :torneo como librería en tu ordenador, consumirla por sus coordenadas, empaquetarla para iOS y vigilar su compatibilidad binaria.70 min
- 309Concurrencia y memoria en Kotlin/NativeVer perder goles a mil corrutinas, arreglarlo de cuatro maneras y entender el recolector de Native y su convivencia con ARC de Swift.50 min
- 310Compose MultiplatformCrear un módulo de interfaz compartida con Compose Multiplatform, verlo en Android y compilarlo para iOS, y decidir cuándo compartir también la interfaz.60 min
Referencia
Chuletas para volver cuando ya no estés aprendiendo, sino trabajando.