Saltar al contenido
K
Cimientos del código compartidoPaso a paso40 min de lectura · 10 min de máquina Intermedio

Corrutinas en commonMain

Al terminar: Escribir código asíncrono compartido con dispatchers inyectados y sin bloquear el hilo principal.

Las corrutinas son lo más portable de Kotlin: el mismo suspend, el mismo Flow y las mismas reglas de cancelación funcionan igual en Android, en iOS y en la JVM. Las viste en Marcador (Corrutinas y suspend, Dispatchers y withContext, Flow frío), y aquí cambia solo dónde viven: en commonMain, el código que compilan a la vez Android, iOS y la JVM.

Hay una piedra en ese camino: Dispatchers.IO, escrito tal cual, no compila en commonMain. Esta lección mide dónde está esa pared, ve el arreglo de una línea, y deja los dispatchers como una dependencia que se inyecta en vez de una constante escrita a pelo. Para ello se añade la segunda frontera internal expect del proyecto, y la lección dice con franqueza si hacía falta.

Y por el camino aparece la trampa que más desconcierta al empezar con iOS: los argumentos por defecto de Kotlin no existen al otro lado.

Los comandos se escriben en la Terminal de Android Studio (View → Tool Windows → Terminal), en ~/Pokedex-KMP. Para crear ficheros, clic derecho sobre el módulo shared → New → File y la ruta completa desde la carpeta del módulo (lo viste en El módulo compartido y la prueba de humo); plan B, touch en la Terminal con esa misma ruta.

La pared: Dispatchers.IO en commonMain

En vez de fiarse de lo que se lee por ahí, se mide. Un fichero de una línea en cada source set (la carpeta de código con su propio conjunto de dependencias; lo definió Qué es KMP y qué no) y se compila para ver el mensaje. Es un ejemplo para leer, no para escribir (se hizo con las corrutinas ya instaladas, que instalas tú en el Paso 1):

ejemplo: la sonda (no la escribas)
package com.sebastiannarvaez.pokedex
 
import kotlinx.coroutines.Dispatchers
 
// `internal`: visible solo dentro del modulo, no sale en el header de Swift.
internal val sonda = Dispatchers.IO

Compilada con corrutinas 1.11.0 en los dos lados que importan, el código común y iOS (./gradlew --continue :shared:compileCommonMainKotlinMetadata :shared:compileKotlinIosSimulatorArm64: la primera tarea compila el código común y la segunda el de iOS; --continue hace que Gradle siga con la otra aunque la primera falle), el resultado según dónde se escriba la línea es:

Dónde se escribe la líneaResultado
commonMainfalla
iosMainfalla
androidMaincompila
jvmMaincompila

El arreglo es una línea. Dispatchers.IO existe para todos los destinos que tienen hilos: Android, la JVM e iOS, que son los de este proyecto. Por eso commonMain puede usarla. Está declarada como propiedad de extensión y, como cualquier cosa de otro paquete, hay que importarla:

ejemplo: la sonda con el import que falta (no la escribas)
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO   // la propiedad de extension: sin ella solo se ve el miembro `internal`

Con ese import, el mismo comando termina en BUILD SUCCESSFUL y compila para los tres destinos.

En el código de la librería, esa propiedad está declarada en la carpeta que comparten la JVM y Native, no en la API común:

kotlinx-coroutines-core, source set concurrent
public expect val Dispatchers.IO: CoroutineDispatcher

Si algún día el proyecto añadiera la web (JavaScript o Wasm), commonMain dejaría de verla. Para no liarse, así son los tres «IO» que aparecen en esta lección:

DóndeQué es
Dentro de Dispatchers, en común y en iOSun miembro internal: el que ve la sonda y no se puede usar
En commonMain, con import kotlinx.coroutines.IOla propiedad de extensión pública: la que sí se puede usar
En androidMain y jvmMainun miembro público de Dispatchers: no hace falta ningún import

Entonces, ¿por qué una frontera expect si basta un import? Hay dos caminos y los dos funcionan: usar Dispatchers.IO con el import en todas partes, o esconder la elección tras una función que cada plataforma implementa. Este proyecto sigue el segundo: deja en un solo sitio qué dispatcher usa cada plataforma para el trabajo que bloquea. La lección expect/actual frente a interfaces dijo que el expect es la excepción y la interfaz es la regla. Con ese filtro, la respuesta honesta es que aquí el expect no hacía falta: el import también resuelve el problema, y por eso el proyecto ya no tiene «una sola» frontera. Se deja para tener la elección del dispatcher en un solo fichero, porque es una decisión por plataforma que puede cambiar al medir (lo verás justo debajo), y para que veas una frontera con tres actual de una línea. En un proyecto tuyo, el import bastaría.

Los dispatchers, como dependencia y no como constante

La razón de que AppDispatchers sea una interfaz y no una constante es el test. Un ejemplo para leer (el AppDispatchers real lo escribes en el Paso 2): un AppDispatchers de mentira, cuyos tres dispatchers apuntan al de prueba, y un test que usa el reloj virtual.

ejemplo: un test con dispatchers de mentira (no lo escribas)
package com.sebastiannarvaez.pokedex
 
import com.sebastiannarvaez.pokedex.core.AppDispatchers
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.currentTime
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
 
// Un AppDispatchers de mentira: los tres apuntan al dispatcher de prueba.
private class DispatchersDeMentira(d: CoroutineDispatcher) : AppDispatchers {
    override val io = d
    override val default = d
    override val main = d
}
 
class AppDispatchersTest {
    @Test
    fun elTiempoEsVirtual() = runTest {
        // `testScheduler` es el reloj virtual de runTest, y
        // `StandardTestDispatcher` es un dispatcher que obedece a ese reloj.
        val dispatchers = DispatchersDeMentira(StandardTestDispatcher(testScheduler))
        // Un trabajo que "tarda" 2 segundos (`2.seconds` es una duracion)
        // en el dispatcher de IO...
        withContext(dispatchers.io) { delay(2.seconds) }
        // ...y el reloj virtual avanza 2000 ms (`currentTime` lo cuenta en
        // milisegundos; la `L` es un entero largo) sin esperar ni uno de verdad.
        assertEquals(2000L, currentTime)
    }
}

Ejecutado en la JVM y en iOS (./gradlew :shared:jvmTest :shared:iosSimulatorArm64Test), pasa en milisegundos: medido, 0,06 s en la JVM y 0,001 s en iOS. Con Dispatchers.IO escrito dentro de la función, ese mismo delay(2.seconds) tardó 2,0 s reales (2013 ms) y el reloj virtual se quedó en 0. Por eso se inyecta. No es código del proyecto, y por eso no lo escribes: los tests de verdad con dispatchers de mentira llegarán cuando haya una clase de datos que los reciba, y de ahí que el número de tests del proyecto no cambie hoy.

Por qué en iOS basta con Default

En el paso 2 verás que el actual de iOS devuelve Dispatchers.Default y los de Android y la JVM, Dispatchers.IO. ¿Esa diferencia es un problema? Se midió: 200 tareas que bloquean su hilo 300 milisegundos cada una, en un Mac de 11 núcleos, contando cuántas corrían a la vez.

PlataformaDefaultIO
JVM y Android11 a la vez (tantas como núcleos)64
iOS (simulador)6464

Así que el problema real es el contrario del que parece: usar Default para trabajo que bloquea en Android o en la JVM agota los hilos reservados para cálculo (tantos como núcleos tiene el procesador) con unas pocas llamadas, y se para también el trabajo de cálculo que debería seguir corriendo. Por eso allí io es Dispatchers.IO. En iOS, Default ya aguanta ese bloqueo, y la elección no cambia nada.

Si algún día midieras algo distinto, cambias el actual de iOS y no se entera nadie más: la decisión vive en un solo sitio.

La trampa: los argumentos por defecto no cruzan

Esta es la lección de KMP que más desconcierta. El primer intento, cuando Pokedex reciba los dispatchers, es el obvio: un valor por defecto, para no tener que pasarlos.

ejemplo: el intento obvio (no lo escribas)
// `= PlatformAppDispatchers()`: un valor por defecto, para no tener que pasarlo.
class Pokedex(internal val dispatchers: AppDispatchers = PlatformAppDispatchers()) {

En Kotlin vale: Pokedex() compila. Pero Xcode, en la línea de ContentView.swift que llama a Pokedex(), dice:

El arreglo es un constructor secundario: un segundo constructor, además del principal, que se declara con constructor(…) y tiene que delegar en otro con : this(…). Un ejemplo mínimo para leer:

ejemplo: un constructor secundario (no lo escribas)
// Constructor principal: va en la cabecera de la clase.
class Caja(val ancho: Int) {
    // Secundario: sin parametros. `: this(10)` lo delega en el principal con 10.
    constructor() : this(10)
}
 
// Caja(5).ancho  vale 5
// Caja().ancho   vale 10  (llama al secundario, que llama al principal)

En el caso de Pokedex, el principal pasa a ser internal y el secundario sin parámetros es el que usan las apps. La regla que conviene guardarte, que vale para cualquier función y no solo para constructores: cada combinación de parámetros que quieras poder llamar desde Swift necesita su propia sobrecarga en Kotlin (una sobrecarga es otra versión de la misma función con otros parámetros).

Reto · Ejercicio: dos formas de crear un temporizador

En una subcarpeta nueva temporizador dentro de shared/src/commonMain/kotlin/com/sebastiannarvaez/pokedex/ejercicios/ (junto a calculadora), crea una clase Temporizador que guarde unos segundos y tenga una función que los describa («Temporizador de N s»). Desde Swift tiene que poder crearse de dos formas: sin argumentos, y entonces son 10 segundos, y con los segundos que tú le pases.

Escríbelo primero con un valor por defecto, compila el framework (./gradlew :shared:linkDebugFrameworkIosSimulatorArm64) y crea en la carpeta Ejercicios de Xcode (iosApp/iosApp/Ejercicios/), en una subcarpeta Temporizador, una vista TemporizadorVista que use las dos formas: Xcode protestará en una. Arréglalo en Kotlin, no en Swift. La vista previa debe enseñar:

Temporizador de 10 s
Temporizador de 5 s
Ver solución

El error del primer intento es el de arriba, 'init()' is unavailable: el header solo trae init(segundos:). Con el arreglo, trae init() e init(segundos:).

shared/src/commonMain/kotlin/com/sebastiannarvaez/pokedex/ejercicios/temporizador/Temporizador.kt
package com.sebastiannarvaez.pokedex.ejercicios.temporizador
 
// Constructor principal: con los segundos.
class Temporizador(val segundos: Int) {
    // Sin argumentos: 10 segundos. Un argumento por defecto no llegaria a Swift.
    constructor() : this(10)
 
    fun describir(): String = "Temporizador de $segundos s"
}
iosApp/iosApp/Ejercicios/Temporizador/TemporizadorVista.swift
import SwiftUI
import Shared
 
struct TemporizadorVista: View {
    var body: some View {
        VStack(spacing: 8) {
            Text(Temporizador().describir())
            Text(Temporizador(segundos: 5).describir())
        }
        .padding()
    }
}
 
#Preview {
    TemporizadorVista()
}

Paso 1 · Instala las corrutinas en el módulo compartido

Objetivo: que el código de shared vea kotlinx.coroutines. Es la receta de instalar una librería de Los ficheros de Gradle de tu proyecto y de la de corrutinas de Corrutinas y suspend, aplicada a un módulo multiplataforma: cambia el sitio de las dependencias, no el método.

Pokedex-KMP
gradle
libs.versions.tomlse modifica: [versions] y [libraries]
shared
build.gradle.ktsse modifica: dependencias
Los dos ficheros de toda instalación: el catálogo y el build.gradle.kts del módulo que usa la librería.

1 · Qué instalas y por qué ahora. kotlinx-coroutines-core trae delay, launch, withContext, Flow y los Dispatchers; kotlinx-coroutines-test trae runTest y el reloj virtual. Hacen falta ya porque esta lección escribe los dispatchers del proyecto, y los tests de las próximas los usarán. (Los ejemplos de arriba se midieron con ellas ya instaladas.)

2 · La versión, en [versions]. Una sola línea para las dos librerías: se publican juntas y suben a la vez. Abre gradle/libs.versions.toml y añade al final de [versions]:

gradle/libs.versions.toml: [versions], línea nueva
coroutines = "1.11.0"

3 · Las librerías, en [libraries]. Al final de esa sección (la cabecera [libraries] ya existe; no la repitas):

gradle/libs.versions.toml: [libraries], líneas nuevas
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }

Cada cosa en su sección: un número suelto nunca va en [libraries].

4 · No es un plugin, así que [plugins] y el build.gradle.kts de la raíz no se tocan.

5 · La dependencia, en el módulo que la usa. Abre shared/build.gradle.kts. En un módulo multiplataforma las dependencias no están en un dependencies { } suelto, sino dentro del source set que las necesita, en sourceSets { … }: commonMain es lo que compilan todas las plataformas y commonTest, los tests comunes. Añade una línea en cada uno de los que ya existen:

build.gradle.kts de shared, dentro de kotlin { }: dependencias nuevas
    sourceSets {
        commonMain.dependencies {
            // implementation: las apps no escriben ningun tipo de corrutinas
            // (la explicacion, debajo del bloque).
            implementation(libs.kotlinx.coroutines.core)
            // … lo que ya habia (kermit con api) se queda igual
        }
        commonTest.dependencies {
            // Ya estamos dentro de commonTest, que solo existe para tests:
            // por eso aqui basta `implementation` (`testImplementation` es la
            // forma de decirlo en un modulo que mezcla codigo y tests).
            implementation(libs.kotlinx.coroutines.test)
            // … lo que ya habia (kotlin-test) se queda igual
        }
    }

api frente a implementation lo viste en La primera dependencia, donde también se define la firma pública. Con las corrutinas hay un matiz que conviene decir con franqueza: AppDispatchers (el Paso 2) es pública y sus propiedades son de tipo CoroutineDispatcher, así que ese tipo asoma en la firma de shared. Con implementation funciona igualmente por dos razones: ninguna app llama a esas propiedades (usan Pokedex(), que no las enseña) y la app de Android ya recibe las corrutinas por otras librerías (Compose las trae). Si una app tuviera que escribir CoroutineDispatcher, lo correcto sería api.

6 · Sync Now, y comprueba. Pulsa Sync Now en el aviso que sale arriba del editor. Debe terminar sin errores rojos. Si sale en rojo, mira la caja «Si Sync sale en rojo» de Los ficheros de Gradle de tu proyecto; aquí lo habitual es un alias mal escrito (libs.kotlinx.coroutines.cor) o una versión que no existe. Para comprobar que la librería está, pídele a Gradle la lista de lo que usa el código de la JVM al ejecutarse:

./gradlew :shared:dependencies --configuration jvmRuntimeClasspath

Debe aparecer org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0 y, debajo, su versión para la JVM, kotlinx-coroutines-core-jvm:1.11.0, que Gradle elige sola.

Paso 2 · Los dispatchers de la app

Se crean cuatro ficheros, uno en común y tres de una línea, uno por plataforma. Primero los ficheros, vacíos, y luego lo que lleva cada uno.

shared/src/commonMain/kotlin/.../core/AppDispatchers.ktse crea
shared/src/androidMain/kotlin/.../core/Dispatchers.android.ktse crea
shared/src/iosMain/kotlin/.../core/Dispatchers.ios.ktse crea
shared/src/jvmMain/kotlin/.../core/Dispatchers.jvm.ktse crea
shared/src/commonMain/kotlin/.../Pokedex.ktse modifica (paso 3)
Una interfaz común, tres implementaciones de una línea y la fachada que las recibe.

La carpeta core ya existe en commonMain (ahí vive Log.kt, de La primera dependencia), pero no en androidMain, iosMain ni jvmMain: al escribir la ruta completa en New → File, Android Studio crea las carpetas que falten. Crea los cuatro ficheros (clic derecho sobre shared → New → File y cada ruta, como en la lista de abajo). Plan B desde la Terminal: mkdir -p crea las carpetas y touch, los ficheros vacíos:

# `mkdir -p` crea tambien las carpetas intermedias; `touch` crea un fichero vacio
mkdir -p shared/src/{commonMain,androidMain,iosMain,jvmMain}/kotlin/com/sebastiannarvaez/pokedex/core
touch shared/src/commonMain/kotlin/com/sebastiannarvaez/pokedex/core/AppDispatchers.kt
touch shared/src/androidMain/kotlin/com/sebastiannarvaez/pokedex/core/Dispatchers.android.kt
touch shared/src/iosMain/kotlin/com/sebastiannarvaez/pokedex/core/Dispatchers.ios.kt
touch shared/src/jvmMain/kotlin/com/sebastiannarvaez/pokedex/core/Dispatchers.jvm.kt

Abre AppDispatchers.kt y escribe. Tiene tres cosas: la interfaz, la implementación real y la frontera internal expect:

shared/src/commonMain/kotlin/com/sebastiannarvaez/pokedex/core/AppDispatchers.kt
package com.sebastiannarvaez.pokedex.core
 
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
 
/**
 * Los tres dispatchers de la app, como dependencia y no como constante.
 *
 * Se inyecta en vez de llamar a `Dispatchers` directamente por una razon muy
 * concreta: en un test se sustituye por el dispatcher de prueba y `runTest`
 * puede adelantar el tiempo virtual. Con `Dispatchers.IO` escrito a pelo dentro
 * de un repositorio, ese test tiene que esperar de verdad.
 */
interface AppDispatchers {
    /** Trabajo que bloquea el hilo: disco y red. */
    val io: CoroutineDispatcher
 
    /** Trabajo que consume CPU: parsear, ordenar, transformar listas. */
    val default: CoroutineDispatcher
 
    /** El hilo de la interfaz. */
    val main: CoroutineDispatcher
}
 
internal class PlatformAppDispatchers : AppDispatchers {
    override val io: CoroutineDispatcher = ioDispatcher()
    override val default: CoroutineDispatcher = Dispatchers.Default
    override val main: CoroutineDispatcher = Dispatchers.Main
}
 
/**
 * El dispatcher para el trabajo que bloquea, elegido por plataforma. Es una
 * funcion `expect` y no `Dispatchers.IO` a pelo para que la eleccion viva en
 * un solo sitio y se pueda cambiar sin tocar el dominio.
 */
internal expect fun ioDispatcher(): CoroutineDispatcher

La interfaz tiene tres propiedades, una por tipo de trabajo. PlatformAppDispatchers es la implementación de verdad: Default y Main salen tal cual de Dispatchers, y el de entrada/salida lo decide ioDispatcher(). Esa función no tiene cuerpo, porque es la parte expect: lo que sigue son sus tres actual, de una línea. El de Android:

shared/src/androidMain/kotlin/com/sebastiannarvaez/pokedex/core/Dispatchers.android.kt
package com.sebastiannarvaez.pokedex.core
 
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
 
internal actual fun ioDispatcher(): CoroutineDispatcher = Dispatchers.IO

El de la JVM es idéntico, en jvmMain (el paquete y el código no cambian):

shared/src/jvmMain/kotlin/com/sebastiannarvaez/pokedex/core/Dispatchers.jvm.kt
package com.sebastiannarvaez.pokedex.core
 
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
 
internal actual fun ioDispatcher(): CoroutineDispatcher = Dispatchers.IO

Y el de iOS es el que lleva la explicación (la tabla de arriba): usa Default.

shared/src/iosMain/kotlin/com/sebastiannarvaez/pokedex/core/Dispatchers.ios.kt
package com.sebastiannarvaez.pokedex.core
 
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
 
/**
 * En iOS se usa `Default`. Con corrutinas 1.11.0, `Default` admite ahi
 * hasta 64 tareas bloqueadas a la vez, igual que `IO`, asi que no hace falta
 * otro: la diferencia entre los dos solo existe en Android y en la JVM.
 */
internal actual fun ioDispatcher(): CoroutineDispatcher = Dispatchers.Default

(Aquí no hace falta el import kotlinx.coroutines.IO de la sonda: es la tercera fila de la tabla de arriba.)

Compila los tres destinos para comprobar que cada actual encaja con su expect:

./gradlew :shared:compileKotlinJvm :shared:compileAndroidMain :shared:compileKotlinIosSimulatorArm64

ioDispatcher() sigue la misma regla que currentPlatform(): mínima, internal, y nadie la llama desde el dominio.

Paso 3 · Los dispatchers entran por la puerta

Ahora los dispatchers tienen que entrar en la puerta de la lección anterior, sin tropezar con la trampa de arriba. Abre Pokedex.kt (el de commonMain) y sustitúyelo por este, que es el fichero entero:

shared/src/commonMain/kotlin/com/sebastiannarvaez/pokedex/Pokedex.kt
package com.sebastiannarvaez.pokedex
 
import com.sebastiannarvaez.pokedex.core.AppDispatchers
import com.sebastiannarvaez.pokedex.core.PlatformAppDispatchers
 
/**
 * La puerta de entrada del modulo compartido.
 *
 * Existe para que cada app tenga **un solo simbolo** que conocer. Sin ella, la
 * interfaz de iOS tendria que llamar a la funcion de nivel superior que resuelve
 * la plataforma, cuyo nombre en Swift sale del fichero donde vive el `actual`.
 *
 * Aqui dentro iran, segun avance el curso, el cliente de red, la base de datos
 * y los repositorios.
 */
class Pokedex internal constructor(
    internal val dispatchers: AppDispatchers,
) {
 
    /**
     * El constructor que usan las dos apps.
     *
     * Es un constructor secundario y no un argumento por defecto porque **los
     * argumentos por defecto no cruzan a Swift**: con `dispatchers` por defecto,
     * el header solo declara `init(dispatchers:)` y `Pokedex()` responde
     * «'init()' is unavailable».
     */
    constructor() : this(PlatformAppDispatchers())
 
    private val platform: Platform = currentPlatform()
 
    fun greeting(): String = Greeting(platform).greet()
}

Compila el framework (./gradlew :shared:linkDebugFrameworkIosSimulatorArm64) y vuelve a Xcode (Cmd+B): sin errores. Ahora el header declara el constructor sin parámetros (y el de los dispatchers, internal, ya no sale):

Shared.h — generado por el compilador (lo relevante)
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));

El constructor principal es internal: las apps solo pueden llamar al de cero argumentos, y quien pasa dispatchers de mentira son los tests, que ven lo internal del mismo módulo.

Punto de control

  • El catálogo tiene coroutines = "1.11.0" en [versions] y las dos librerías, core y test, en [libraries]; shared las usa en commonMain y commonTest.
  • ./gradlew :shared:dependencies --configuration jvmRuntimeClasspath lista kotlinx-coroutines-core:1.11.0.
  • Existe core/AppDispatchers.kt con la interfaz, PlatformAppDispatchers y el internal expect fun ioDispatcher().
  • Hay tres actual de ioDispatcher(): Dispatchers.IO en Android y en la JVM, Dispatchers.Default en iOS.
  • Pokedex tiene constructor primario internal con dispatchers y un constructor secundario sin argumentos.
  • En Shared.h, Pokedex declara init(). Si solo declara init(dispatchers:), te ha quedado el argumento por defecto.
  • ./gradlew :shared:jvmTest :shared:testAndroidHostTest :shared:iosSimulatorArm64Test sigue con 7 tests en cada plataforma si tienes tus ejercicios (los 2 de GreetingTest, los 3 de PokemonTest y los 2 de la batería) y con 5 si no.
  • Las dos apps siguen arrancando y enseñando su saludo.

Comprueba lo que entendiste

7 preguntas · necesitas 5 para completar la lección

  1. 1.Con corrutinas 1.11.0, ¿qué responde el compilador de iOS al usar Dispatchers.IO desde commonMain, sin ningún import extra?

  2. 2.La sonda internal val sonda = Dispatchers.IO se compiló en los cuatro source sets. ¿Dónde falló?

  3. 3.¿Por qué AppDispatchers es una interfaz inyectada y no una constante llamada dentro de cada clase?

  4. 4.class Pokedex(internal val dispatchers: AppDispatchers = PlatformAppDispatchers()) compila en Kotlin, y Swift responde 'init()' is unavailable. ¿Por qué?

  5. 5.¿Cuál es la regla general que se saca de esa trampa, más allá de los constructores?

  6. 6.En iOS, el actual de ioDispatcher() devuelve Dispatchers.Default. ¿Por qué no es un problema?

  7. 7.Un Pokedex tiene un constructor principal internal con los dispatchers y un constructor secundario sin parámetros. ¿Qué constructor ve Swift en Shared.h?

Faltan 7 de 7