Saltar al contenido
K
Autenticación con SupabasePaso a paso38 min Intermedio

Configuración y secretos por entorno

Al terminar: Llevar la URL y la clave hasta las dos apps sin escribirlas en el código.

Tienes dos valores del panel de Supabase apuntados en un bloc de notas. Esta lección los lleva hasta las dos apps sin escribirlos nunca dentro del código, y deja la app compilando y funcionando aunque esos dos valores estén vacíos.

Esa última parte es la que más código cuesta y la que más se agradece. Un proyecto que no compila hasta que te registras en un servicio de terceros es un proyecto que nadie prueba.

Un entorno es cada sitio donde corre la app con sus propios datos: tu ordenador mientras la escribes, el de otra persona que la prueba, la versión publicada. Cada uno puede apuntar a un proyecto de Supabase distinto, así que la dirección y la clave no pueden ir escritas en el código: cada entorno pone las suyas. De eso trata la lección.

No se instala nada: usas Gradle, Koin y Xcode tal como están. Todos los comandos se escriben en la Terminal de Android Studio, en la carpeta de tu proyecto.

Lo que esto no hace

Conviene decirlo antes que nada, porque se malinterpreta siempre.

Sacar una clave del código no la oculta de un atacante. Los dos valores acaban dentro del binario, y cualquiera con el APK en la mano los saca en un minuto. Por eso la clave que se usa es la pública, que está pensada para eso.

Lo que sí consigue es que la clave no esté escrita en medio del código fuente, que es lo que se copia, se comparte y se enseña en una captura: viven en un fichero de ajustes por plataforma, y el código solo los lee.

Los dos ficheros no son iguales. local.properties es de tu máquina por diseño: Android Studio lo escribe y no se comparte. En Los ficheros de Gradle de tu proyecto se decía que no se toca porque solo lleva la ruta del SDK; aquí añadimos dos líneas debajo de la suya, y Android Studio no las borra. Config.xcconfig, en cambio, es un fichero del proyecto (lo trae la plantilla y ya lo viste en Ejecutar en iOS): si algún día compartes tu carpeta entera, saldría con tus claves dentro, así que antes de compartirla las vacías.

AndroidiOS
Dónde están tus valoreslocal.propertiesConfig.xcconfig
Cómo llegan al códigoGradle los convierte en constantes de BuildConfigel Info.plist los copia (cambia cada $(NOMBRE) por su valor) y el código los lee del bundle

(El bundle es el paquete de la app ya instalada, con sus recursos.)

shared
src/commonMain/kotlin/com/sebastiannarvaez/pokedex
core/AppConfig.ktse sustituye (paso 1)
di/Modules.ktse quita una línea (paso 1)
src/commonTest/kotlin/com/sebastiannarvaez/pokedex
data/network/KtorPokeApiTest.kt, ReintentosTest.ktdejan de tener su doble (paso 2)
dobles/ConfigDePrueba.ktse crea: el doble único (paso 2)
src/iosMain/kotlin/com/sebastiannarvaez/pokedex/di/KoinIos.ktse sustituye (paso 4)
local.propertiestus dos valores (paso 3)
androidApp
build.gradle.ktslos lee (paso 3)
src/main/kotlin/com/sebastiannarvaez/pokedex/android
core/AndroidAppConfig.ktse crea (paso 3)
PokedexApplication.ktla entrega al grafo (paso 3)
iosApp
Configuration/Config.xcconfigtus dos valores (paso 4)
iosApp/Info.plist, Core/IosAppConfig.swift, iOSApp.swiftlos leen (paso 4)
Qué se modifica y qué se crea. Vista Project; los ficheros de iOS, en el Project navigator de Xcode.

Paso 1 · La interfaz en el núcleo

El archivo. AppConfig.kt ya existe: lo creaste en HTTP y Ktor: el cliente de PokeAPI con una sola dirección, la de PokeAPI. Ahora tiene que saber también de Supabase.

Ábrelo: shared/src/commonMain/kotlin/com/sebastiannarvaez/pokedex/core/AppConfig.kt (o Navigate → File…, Cmd+Shift+O, y su nombre). Es corto y lo escribiste tú, así que se sustituye entero: selecciona todo (Cmd+A) y escribe. Ojo: la clase DefaultAppConfig, que estaba al final del fichero, desaparece con este cambio, y el grafo de Koin la usa: Modules.kt se pondrá en rojo hasta que hagas lo que sigue al código.

shared/src/commonMain/kotlin/com/sebastiannarvaez/pokedex/core/AppConfig.kt
package com.sebastiannarvaez.pokedex.core
 
// Lo que cambia entre entornos y entre desarrolladores. Es una interfaz porque
// las claves no pueden ser constantes en el código: cada app las lee del sitio
// donde su plataforma las guarda.
interface AppConfig {
    // La dirección de PokeAPI: no es secreta y no cambia.
    val pokeApiBaseUrl: String
 
    // El proyecto de Supabase. Vacío si no se ha configurado.
    val supabaseUrl: String
 
    // La clave pública. Nunca la de servicio.
    val supabaseKey: String
 
    // Si la parte de cuenta está disponible. Una propiedad con `get()` se
    // calcula cada vez que se lee. La app funciona sin cuenta: si falta
    // alguno de los dos valores, la pantalla de sesión se esconde y el resto
    // va igual.
    val haySupabase: Boolean
        get() = supabaseUrl.isNotBlank() && supabaseKey.isNotBlank()
}
 
// Una base con lo que es igual en todas partes: la dirección de PokeAPI. Las
// apps heredan de ella y rellenan solo las dos claves.
abstract class BaseAppConfig : AppConfig {
    override val pokeApiBaseUrl: String = "https://pokeapi.co/api/v2/"
}

Dos decisiones que parecen menores y no lo son.

haySupabase vive aquí y no en cada app. Es una regla —«hay cuenta si hay las dos cosas»— y una regla escrita dos veces se desincroniza. La usarán la raíz de Android, la raíz de iOS y la pantalla de ajustes.

BaseAppConfig existe para los dobles de prueba. Un doble que solo quiere fijar Supabase no debería tener que declarar también la URL de PokeAPI. Lo ves en el paso siguiente.

Una cosa más: DefaultAppConfig, la clase que tenías al final del fichero, ya no existe. Y el grafo de Koin la registraba. Abre shared/src/commonMain/kotlin/com/sebastiannarvaez/pokedex/di/Modules.kt y borra el import de DefaultAppConfig y esta línea de pokedexModule. El import de AppConfig déjalo: Android Studio lo pondrá en gris (sin uso) durante un rato, y no es un error; las lecciones siguientes vuelven a usarlo.

shared/src/commonMain/kotlin/com/sebastiannarvaez/pokedex/di/Modules.kt: la línea que se borra
    single<AppConfig> { DefaultAppConfig() }

Es deliberado: si el grafo trae una configuración de repuesto, una app que se olvide de pasar la suya funciona a medias y nadie se entera. Mejor que falle al arrancar. A partir de ahora cada app aporta la suya al arrancar Koin (pasos 3 y 4).

Paso 2 · Lo que se rompe: los dobles de prueba

Añadir dos propiedades a una interfaz rompe a todo el que la implementaba. Lanza los tests del núcleo:

./gradlew :shared:jvmTest

En vez de arreglar los dos, los sustituyes por uno solo: dos dobles que hacen lo mismo son dos sitios que mantener cada vez que AppConfig cambie. Hereda de BaseAppConfig, que ya trae la URL de PokeAPI, y por eso solo declara las dos claves. Primero créalo: clic derecho sobre la carpeta shared → New → File → src/commonTest/kotlin/com/sebastiannarvaez/pokedex/dobles/ConfigDePrueba.kt (la carpeta dobles ya existe, de El puente de paginación para iOS):

shared/src/commonTest/kotlin/com/sebastiannarvaez/pokedex/dobles/ConfigDePrueba.kt
package com.sebastiannarvaez.pokedex.dobles
 
import com.sebastiannarvaez.pokedex.core.BaseAppConfig
 
// La configuración de mentira de todos los tests. La URL de PokeAPI la hereda
// de `BaseAppConfig`; las dos claves llevan un valor cualquiera, y se pueden
// cambiar al construirla: `ConfigDePrueba(supabaseUrl = "")`. Cada `override
// val` entre paréntesis es a la vez un parámetro del constructor y la propiedad
// que cumple la interfaz. `internal`: solo la ven los tests de este módulo.
internal class ConfigDePrueba(
    override val supabaseUrl: String = "https://proyecto.supabase.co",
    override val supabaseKey: String = "clave-publica",
) : BaseAppConfig()

Después, quita los dos dobles viejos. Abre la carpeta shared/src/commonTest/kotlin/com/sebastiannarvaez/pokedex/data/network/ y haz estos cambios, en este orden:

  1. KtorPokeApiTest.kt. Borra la clase FakeConfig entera, con el comentario que tiene encima:

    shared/src/commonTest/kotlin/com/sebastiannarvaez/pokedex/data/network/KtorPokeApiTest.kt: lo que se borra
    // Una configuración de mentira con la misma dirección que la de verdad.
    // `private`: solo la ve este fichero.
    private class FakeConfig : AppConfig {
        override val pokeApiBaseUrl = "https://pokeapi.co/api/v2/"
    }

    Después, en los import, cambia import com.sebastiannarvaez.pokedex.core.AppConfig por import com.sebastiannarvaez.pokedex.dobles.ConfigDePrueba, y en la función api, la línea createHttpClient(FakeConfig(), engine = engine) pasa a createHttpClient(ConfigDePrueba(), engine = engine).

  2. ReintentosTest.kt. Borra la clase ConfigDePrueba privada (con su comentario):

    shared/src/commonTest/kotlin/com/sebastiannarvaez/pokedex/data/network/ReintentosTest.kt: lo que se borra
    // Una configuración de mentira: solo hace falta la dirección base.
    private class ConfigDePrueba : AppConfig {
        override val pokeApiBaseUrl = "https://pokeapi.co/api/v2/"
    }

    Y en los import, quita import com.sebastiannarvaez.pokedex.core.AppConfig y añade import com.sebastiannarvaez.pokedex.dobles.ConfigDePrueba. Los tres ConfigDePrueba() de los tests no cambian: ahora son los del fichero nuevo.

Vuelve a lanzar ./gradlew :shared:jvmTest: debe terminar en BUILD SUCCESSFUL. Es el argumento entero a favor de la clase base: la próxima propiedad que se añada no romperá a ningún doble. Autenticación en el núcleo: la API usará este mismo ConfigDePrueba sin recrearlo.

Paso 3 · Android: de local.properties a BuildConfig

El archivo que guarda tus valores. local.properties es el fichero de ajustes de tu máquina: Android Studio ya lo usa para recordar dónde está el SDK, y por diseño no se comparte con nadie. Es el sitio natural para dos valores que son tuyos. Está en la carpeta raíz del proyecto, entre los ficheros sueltos y cerca de settings.gradle.kts: local.properties. Ábrelo y añade, debajo de la línea sdk.dir, tus dos valores (los de abajo son de ejemplo; pon los tuyos):

local.properties: las dos líneas nuevas
SUPABASE_URL=https://abcdefgh.supabase.co
SUPABASE_KEY=sb_publishable_xxxxxxxx

Sin comillas ni espacios alrededor del =. Si todavía no tienes cuenta, déjalas vacías (SUPABASE_URL=): la app compila igual.

El concepto. Gradle puede convertir esos dos valores en constantes de código: genera una clase BuildConfig y buildConfigField añade una constante propia. Para leerlos, usa Properties, una clase del mundo de Java (el lenguaje sobre el que nació Android; Kotlin puede usar sus clases tal cual) que lee ficheros de ajustes con la forma clave=valor. Un ejemplo ajeno al proyecto, el fichero de ajustes de una cafetería:

ejemplo mínimo: leer un fichero de ajustes con Properties (con comentarios)
import java.util.Properties
 
fun main() {
    // El fichero de ajustes de una cafetería, con forma clave=valor.
    val texto = """
        HORARIO=8:00-20:00
        WIFI=cafe-del-puerto
    """.trimIndent()
 
    // Properties lee ese formato; `apply` lo crea y lo carga en el mismo paso.
    // `texto.reader()` convierte el texto en algo que `load` sabe leer, como
    // si fuera un fichero.
    val ajustes = Properties().apply { load(texto.reader()) }
 
    println(ajustes.getProperty("WIFI"))                 // una clave que está
    println(ajustes.getProperty("TELEFONO"))             // una que falta: null
    println(ajustes.getProperty("TELEFONO", ""))         // la misma, con valor de repuesto
    println("\"${ajustes.getProperty("TELEFONO")}\"")    // null metido en un texto
}

Esto es lo que sale (la tercera línea está vacía):

cafe-del-puerto
null
 
"null"

Fíjate en la última línea: una clave que falta, metida dentro de un texto, se convierte en el texto null, y ese texto no está vacío. Es exactamente lo que pasaría con BuildConfig si olvidas el segundo argumento de getProperty; la caja «getProperty lleva segundo argumento» de más abajo lo cuenta.

El fichero que las lee. Abre androidApp/build.gradle.kts. Son cuatro cambios, y el resto del fichero se queda como está.

Primero, la primera línea del fichero, antes de plugins:

androidApp/build.gradle.kts: primera línea
// `Properties` es la clase de Java que lee ficheros de ajustes `clave=valor`.
import java.util.Properties

Después, entre el bloque plugins { … } y el bloque android { … }:

androidApp/build.gradle.kts: entre plugins y android
// Lee `local.properties` (en la carpeta raíz del proyecto). `rootProject.file`
// da el fichero de esa carpeta, `inputStream()` lo abre para leerlo y `load`
// vuelca sus líneas `clave=valor` en `secretos`. Si no existe, `secretos` queda
// vacío y la app compila igual: lo único que pasa es que no hay cuenta.
val secretos = Properties().apply {
    val fichero = rootProject.file("local.properties")
    if (fichero.exists()) fichero.inputStream().use { load(it) }
}

Tercero, dentro de android { … }, al final del bloque defaultConfig { … } (después de versionName, sin tocar lo demás), dos constantes:

androidApp/build.gradle.kts: al final de defaultConfig
        // Cada constante acaba en BuildConfig.SUPABASE_URL / SUPABASE_KEY. El
        // segundo argumento de getProperty es el valor si falta la clave; las
        // comillas escapadas hacen que la constante sea un texto en Java.
        buildConfigField("String", "SUPABASE_URL", "\"${secretos.getProperty("SUPABASE_URL", "")}\"")
        buildConfigField("String", "SUPABASE_KEY", "\"${secretos.getProperty("SUPABASE_KEY", "")}\"")

Y cuarto, en el bloque buildFeatures { … } que ya existe (con compose = true dentro), añade solo la línea de buildConfig:

androidApp/build.gradle.kts: una línea en buildFeatures
        // Gradle no genera BuildConfig salvo que se lo pidas.
        buildConfig = true

Las comillas escapadas merecen una frase. buildConfigField escribe la constante tal cual en un fichero de Java, y en Java un texto va entre comillas. \" es una comilla dentro de un texto de Kotlin: sin ellas, Gradle escribiría SUPABASE_URL = https://…;, que no es Java válido.

Pulsa Sync Now. Si sale en rojo, la caja «Si Sync sale en rojo» de Los ficheros de Gradle de tu proyecto lista los fallos habituales; los dos propios de esta lección están justo debajo.

La implementación. Es de tres líneas. Clic derecho sobre la carpeta androidApp → New → File y la ruta desde la carpeta del módulo (el método de El módulo compartido y la prueba de humo):

src/main/kotlin/com/sebastiannarvaez/pokedex/android/core/AndroidAppConfig.kt
androidApp/src/main/kotlin/com/sebastiannarvaez/pokedex/android/core/AndroidAppConfig.kt
package com.sebastiannarvaez.pokedex.android.core
 
// BuildConfig lo genera Gradle en este mismo paquete (el `namespace` de
// androidApp). Si tu `namespace` es otro, usa el tuyo en el paquete y aquí.
import com.sebastiannarvaez.pokedex.android.BuildConfig
import com.sebastiannarvaez.pokedex.core.BaseAppConfig
 
// La configuración de Android: hereda la URL de PokeAPI y rellena las dos
// claves con las constantes que Gradle sacó de `local.properties`.
class AndroidAppConfig : BaseAppConfig() {
    override val supabaseUrl: String = BuildConfig.SUPABASE_URL
    override val supabaseKey: String = BuildConfig.SUPABASE_KEY
}

Si BuildConfig sale en rojo, el Sync de arriba no terminó bien.

Entregarla al grafo. Abre androidApp/src/main/kotlin/com/sebastiannarvaez/pokedex/android/PokedexApplication.kt (la que creaste en Arrancar Koin desde Android y desde iOS). Añade estos tres import (el de initKoin ya lo tenías):

androidApp/src/main/kotlin/com/sebastiannarvaez/pokedex/android/PokedexApplication.kt: imports nuevos
import com.sebastiannarvaez.pokedex.android.core.AndroidAppConfig
import com.sebastiannarvaez.pokedex.core.AppConfig
import org.koin.dsl.module

Y sustituye la línea initKoin { … } de onCreate por:

androidApp/src/main/kotlin/com/sebastiannarvaez/pokedex/android/PokedexApplication.kt: initKoin
        initKoin {
            // `this@PokedexApplication` es esta clase: dentro de la lambda de
            // `initKoin`, `this` ya es otra cosa, y la etiqueta dice a cuál nos
            // referimos.
            androidContext(this@PokedexApplication)
            // Un módulo de Koin de una sola línea: «cuando alguien pida un
            // AppConfig, entrega ESTE». La aporta la app y no `shared`: las
            // claves viven donde cada plataforma las guarda.
            modules(module { single<AppConfig> { AndroidAppConfig() } })
        }

La línea crearCanalDiario(this) de debajo, que añadiste en Notificaciones en Android: el canal y el aviso, se queda.

Compruébalo. Pulsa Run (el emulador de Ejecutar en Android): la app arranca con los valores puestos y también con los dos vacíos. Para ver que los valores llegan, añade temporalmente una línea debajo de initKoin { … }:

androidApp/src/main/kotlin/com/sebastiannarvaez/pokedex/android/PokedexApplication.kt: línea temporal
        android.util.Log.i("Config", "Supabase: " + AndroidAppConfig().supabaseUrl)

Vuelve a ejecutar y filtra el Logcat por tag:Config: debe salir tu URL completa, con las dos barras de https://. Borra la línea.

Si cambias tus valores más adelante, basta volver a ejecutar: Gradle vuelve a leer local.properties.

Paso 4 · iOS: del .xcconfig al Info.plist

El camino es el mismo con otras piezas: un fichero de ajustes de Xcode define variables, el Info.plist las expande y el código las lee del bundle. Empieza por el lado de Kotlin.

La función de arranque. Abre shared/src/iosMain/kotlin/com/sebastiannarvaez/pokedex/di/KoinIos.kt (la de Arrancar Koin desde Android y desde iOS). Ahora recibe la configuración y la mete en el grafo igual que Android. Sustituye el fichero entero:

shared/src/iosMain/kotlin/com/sebastiannarvaez/pokedex/di/KoinIos.kt
package com.sebastiannarvaez.pokedex.di
 
import com.sebastiannarvaez.pokedex.core.AppConfig
import org.koin.dsl.module
 
// El arranque de Koin para iOS: la única llamada que hace Swift. Se llama
// `startKoinIos` y no `initKoin` porque `init` + mayúscula es un prefijo
// reservado de Objective-C, el lenguaje con el que Apple escribía apps antes
// de Swift y por el que Kotlin se entiende con Swift (llegaría como `doInitKoin`).
fun startKoinIos(config: AppConfig) {
    initKoin {
        // Igual que en Android: la configuración la aporta la app. Aquí no hace
        // falta `<AppConfig>`: `config` ya es de ese tipo.
        modules(module { single { config } })
    }
}

El fichero que guarda tus valores. Config.xcconfig es el fichero de ajustes de Xcode que viste en Ejecutar en iOS (ahí está tu TEAM_ID). En Xcode: Project navigator → iosApp → Configuration → Config.xcconfig. Añade al final:

iosApp/Configuration/Config.xcconfig: las dos líneas nuevas
// Tus dos valores de Supabase. Vacíos, la app compila y funciona sin cuenta.
// En la dirección, las dos barras de https:// necesitan el escape que explica
// la caja de abajo.
SUPABASE_URL=
SUPABASE_KEY=

Pon tu dirección con el escape (https:/$()/…) y tu clave tal cual.

Es peor que un error de compilación por tres motivos. Uno, nadie avisa: ni Xcode, ni el Info.plist, ni el arranque. Dos, el valor no está vacío, así que haySupabase devuelve true y la app enseña la pantalla de sesión como si todo estuviera bien. Y tres, el fallo aparece muy lejos del sitio donde está la causa: en una petición de red que responde con un error de host desconocido, a veinte ficheros de distancia del .xcconfig. Por eso el punto de control pide imprimir la URL completa en vez de dar por bueno que se configuró bien.

El Info.plist las expande. Clic derecho sobre iosApp → iosApp → Info.plist → Open As → Source Code (así se ve como texto). Dentro del <dict> principal, antes del </dict> final, añade:

iosApp/iosApp/Info.plist: las dos claves nuevas
	<!-- Estas dos entradas copian al Info.plist las variables del Config.xcconfig. -->
	<key>SUPABASE_URL</key>
	<string>$(SUPABASE_URL)</string>
	<key>SUPABASE_KEY</key>
	<string>$(SUPABASE_KEY)</string>

El código que las lee. Primero la carpeta Core, que no existe todavía. Créala desde la Terminal de Android Studio, en la carpeta de tu proyecto (es el método alternativo de La lista en iOS, más rápido que los menús):

mkdir -p iosApp/iosApp/Core

Xcode la muestra sola en el Project navigator (la carpeta de la app está sincronizada con el disco). Si no aparece, ciérralo con Cmd+Q y ábrelo otra vez.

Antes del fichero, una idea nueva: en Swift, una clase puede heredar de otra y sobrescribir una propiedad con una calculada. Un ejemplo ajeno al proyecto, con las tiendas de una cafetería (lo puedes probar en un Playground de Xcode):

ejemplo mínimo: una base con lo común y una propiedad calculada que la sobrescribe (con comentarios)
// Una cafetería con lo que es igual en todas sus tiendas.
class TiendaBase {
    let horario = "8:00-20:00"        // igual en todas: lo trae la base
    var wifi: String { "" }           // cada tienda pone la suya
}
 
// La ficha de la tienda, como un Info.plist: claves con valores de cualquier tipo.
let ficha: [String: Any] = ["WIFI": "cafe-del-puerto", "MESAS": 12]
 
class TiendaDelPuerto: TiendaBase {
    // `override var` con llaves: una propiedad calculada que se ejecuta al leerla.
    override var wifi: String {
        ficha["WIFI"] as? String ?? ""   // `as? String`: «si es texto»; `?? ""`: si no, vacío
    }
}
 
let tienda = TiendaDelPuerto()
print(tienda.horario)
print(tienda.wifi)
print(ficha["MESAS"] as? String ?? "(no es texto)")
print(ficha["TELEFONO"] as? String ?? "(falta)")

Esto es lo que sale:

8:00-20:00
cafe-del-puerto
(no es texto)
(falta)

MESAS existe pero es un número, no un texto, así que as? String falla y se usa el repuesto; TELEFONO no existe y pasa lo mismo. Del lado de Swift, BaseAppConfig es una clase como TiendaBase: IosAppConfig hereda la URL de PokeAPI y pone las dos claves.

Ahora el fichero. En Xcode: clic derecho sobre la carpeta Core → New File from Template… → Swift File → Next, nombre IosAppConfig → Create:

iosApp/iosApp/Core/IosAppConfig.swift
import Foundation
import Shared
 
// La configuración de iOS, leída del Info.plist. `BaseAppConfig` es la clase
// de Kotlin del paso 1, y una clase de Swift puede heredar de una de Kotlin:
// llega a Swift como una clase más, de la que se hereda con `:` y a la que se
// sobrescribe con `override`.
class IosAppConfig: BaseAppConfig {
    // Un `val` de Kotlin llega a Swift como una propiedad de solo lectura, y se
    // sobrescribe con una propiedad calculada (`override var` y un cuerpo con
    // llaves): cada vez que se lee, ejecuta el cuerpo. `Bundle.main` es el
    // paquete de la app en marcha; `object(forInfoDictionaryKey:)` lee una
    // clave del Info.plist y la devuelve como `Any?`; `as? String` la convierte
    // a texto y `?? ""` pone un texto vacío si faltaba.
    override var supabaseUrl: String {
        Bundle.main.object(forInfoDictionaryKey: "SUPABASE_URL") as? String ?? ""
    }
 
    override var supabaseKey: String {
        Bundle.main.object(forInfoDictionaryKey: "SUPABASE_KEY") as? String ?? ""
    }
}

Y se la pasas a Kotlin al arrancar. En iosApp → iosApp → iOSApp.swift, cambia la llamada del init() (el resto se queda, incluida la línea del delegado de notificaciones que hay debajo):

iosApp/iosApp/iOSApp.swift: la llamada nueva
        // Ahora recibe la configuración (la etiqueta `config:` viene del
        // parámetro de la función de Kotlin).
        KoinIosKt.startKoinIos(config: IosAppConfig())

Compila con Cmd+B y ejecuta con Cmd+R. Con los dos valores vacíos la app arranca igual que antes.

Comprobar el trayecto entero

Que compile no prueba que el valor llegue. Añade temporalmente, debajo de la llamada anterior en init():

iosApp/iosApp/iOSApp.swift: línea temporal
        print("Supabase:", IosAppConfig().supabaseUrl)

Ejecuta: la consola de Xcode (abajo) debe imprimir tu URL completa. Borra la línea.

Punto de control

  • ./gradlew :shared:jvmTest pasa: el doble único, ConfigDePrueba, hereda de BaseAppConfig.
  • Siguen los mismos tests: no hay ninguno nuevo. Lanza las tres tareas sin filtro y abre los informes (./gradlew :shared:jvmTest :shared:testAndroidHostTest :shared:iosSimulatorArm64Test, y open shared/build/reports/tests/jvmTest/index.html, y los de testAndroidHostTest y iosSimulatorArm64Test): salen 80 en la JVM, 44 en Android host y 47 en iOS.
  • Con los dos valores vacíos, ./gradlew :androidApp:assembleDebug pasa y las dos apps arrancan.
  • Con los valores puestos, el Logcat de Android y la consola de Xcode imprimen la URL completa, con las dos barras.
  • Tus claves están solo en local.properties y en Config.xcconfig: ningún fichero de código las contiene.

Comprueba lo que entendiste

6 preguntas · necesitas 5 para completar la lección

  1. 1.Sacar la clave del código y meterla en local.properties o en Config.xcconfig, ¿qué consigue exactamente?

  2. 2.¿Por qué build.gradle.kts lleva import java.util.Properties en vez de escribir java.util.Properties() entero en el código?

  3. 3.En un .xcconfig, ¿qué pasa al escribir SUPABASE_URL=https://abc.supabase.co?

  4. 4.¿Por qué haySupabase es una propiedad de la interfaz y no un if en cada app?

  5. 5.Añadir dos propiedades a AppConfig rompió los dos dobles de prueba que la implementaban. ¿Cuál es el arreglo bueno?

  6. 6.Tu local.properties tiene SUPABASE_URL= y nada más: no hay línea SUPABASE_KEY. ¿Qué vale BuildConfig.SUPABASE_KEY?

Faltan 6 de 6