Isolated Context in Compose
This page shows how to make Compose functions resolve from an isolated Koin instance with KoinIsolatedContext. Use it when you build an SDK or a library that ships Composables and must not share the host app's Koin container. What an isolated context is, and how to create one, is explained in Context Isolation.
KoinIsolatedContext
Keep the isolated KoinApplication in a static holder, as described in Context Isolation:
- Compiler Plugin DSL
- Annotations
- Constructor / Classic DSL
object PaymentSdkKoin {
val koinApp = koinApplication {
modules(paymentModule)
}
}
internal val paymentModule = module {
single<PaymentProcessor>()
viewModel<PaymentViewModel>()
}
import org.koin.plugin.module.dsl.koinApplication
@Module
@ComponentScan("com.example.paymentsdk")
internal class PaymentModule
@KoinApplication(modules = [PaymentModule::class])
internal object PaymentSdkApp
object PaymentSdkKoin {
val koinApp = koinApplication<PaymentSdkApp>()
}
object PaymentSdkKoin {
val koinApp = koinApplication {
modules(paymentModule)
}
}
internal val paymentModule = module {
singleOf(::PaymentProcessor)
viewModelOf(::PaymentViewModel)
}
Wrap the SDK UI with KoinIsolatedContext:
import org.koin.compose.KoinIsolatedContext
@Composable
fun PaymentScreen(amount: Double, onDone: (PaymentResult) -> Unit) {
KoinIsolatedContext(context = PaymentSdkKoin.koinApp) {
PaymentFlow(amount, onDone)
}
}
@Composable
private fun PaymentFlow(amount: Double, onDone: (PaymentResult) -> Unit) {
val viewModel = koinViewModel<PaymentViewModel>() // resolved from the SDK's Koin
// ...
}
Inside the block, koinInject(), koinViewModel(), getKoin(), currentKoinScope() and rememberKoinModules() use the isolated instance. Outside the block, the host app keeps its own Koin.
Host and SDK in the same screen
Resolve host dependencies outside the block, and SDK dependencies inside:
@Composable
fun CheckoutScreen() {
val analytics = koinInject<HostAnalytics>() // host app's Koin
KoinIsolatedContext(context = PaymentSdkKoin.koinApp) {
val processor = koinInject<PaymentProcessor>() // SDK's Koin
PaymentButton(processor, onEvent = { analytics.track(it) })
}
}
The two containers don't see each other. Pass values across the boundary with parameters and callbacks.
Initialization and shutdown
When the SDK needs configuration from the host, create the KoinApplication in an initialize function and close it on shutdown:
object PaymentSdk {
private var koinApp: KoinApplication? = null
fun initialize(config: PaymentConfig) {
koinApp = koinApplication {
modules(module { single { config } }, paymentModule)
}
}
fun shutdown() {
koinApp?.close()
koinApp = null
}
@Composable
fun PaymentScreen(amount: Double, onDone: (PaymentResult) -> Unit) {
val app = koinApp ?: error("PaymentSdk.initialize() was not called")
KoinIsolatedContext(context = app) {
PaymentFlow(amount, onDone)
}
}
}
The host calls PaymentSdk.initialize(...) once, for example in Application.onCreate(), before any SDK screen is shown.
Limits
- KoinNavigationScope: it creates its scope in the global Koin instance, so it does not follow
KoinIsolatedContext. UseKoinScopeinside the isolated block instead (see Scopes in Compose). - Code outside Compose: SDK classes receive their dependencies through their constructors, from the isolated instance. An SDK entry point outside Compose (a
Service, a callback called by the host) that must retrieve dependencies itself uses aKoinComponentthat overridesgetKoin(). See Context Isolation. - Lifecycle:
KoinIsolatedContextdoes not start or stop the instance. You own its lifetime.
Related
- Context Isolation: create an isolated Koin instance and a custom
KoinComponent. - Koin for Compose:
KoinApplicationandkoinInject(). - Scopes in Compose: scopes inside an isolated block.
- Isolated Context in Ktor: the same concept for a Ktor server.