Isolated Context in Compose
Koin's isolated context allows you to run a separate Koin instance that doesn't interfere with the host application's Koin configuration. This is essential for SDKs, libraries, and white-label applications.
Use Cases
- SDK Development - Your SDK has its own dependencies without affecting the host app
- White-Label Apps - Multiple app variants with different configurations
- Testing - Isolated test configurations
- Feature Modules - Self-contained feature modules with their own DI
Creating an Isolated Context
Define the Context Holder
Create an object to hold your isolated Koin instance:
object MySDKKoinContext {
val koinApp = koinApplication {
// Your SDK modules
modules(
sdkCoreModule,
sdkNetworkModule,
sdkRepositoryModule
)
}
// Convenience accessor
val koin: Koin get() = koinApp.koin
}
SDK Module Example
val sdkCoreModule = module {
single<SDKConfiguration>()
single<SDKLogger>()
}
val sdkNetworkModule = module {
single<SDKApiClient>()
single<SDKAuthManager>()
}
val sdkRepositoryModule = module {
single<SDKDataRepository>()
}
Using with Compose
KoinIsolatedContext
Wrap your SDK's Compose UI with KoinIsolatedContext:
@Composable
fun MySDKScreen() {
KoinIsolatedContext(context = MySDKKoinContext.koinApp) {
// All Koin APIs use the isolated context
SDKContent()
}
}
@Composable
private fun SDKContent() {
// These resolve from the isolated context
val config = koinInject<SDKConfiguration>()
val viewModel = koinViewModel<SDKViewModel>()
Column {
Text("SDK Version: ${config.version}")
SDKFeatureScreen(viewModel)
}
}
Nested Contexts
You can nest isolated contexts:
@Composable
fun HostApp() {
// Host app's Koin context (from startKoin)
val hostService = koinInject<HostService>()
Column {
Text("Host App")
// SDK uses its own isolated context
KoinIsolatedContext(context = MySDKKoinContext.koinApp) {
MySDKScreen()
}
// Back to host context
AnotherHostScreen()
}
}
Lifecycle Management
Manual Initialization
Initialize your SDK context when needed:
object MySDK {
private var _koinContext: KoinApplication? = null
val koinContext: KoinApplication
get() = _koinContext ?: error("SDK not initialized")
fun initialize(config: SDKConfig) {
_koinContext = koinApplication {
modules(
module {
single { config }
},
sdkCoreModule,
sdkNetworkModule
)
}
}
fun shutdown() {
_koinContext?.close()
_koinContext = null
}
}
Usage with Manual Lifecycle
// Host app initializes SDK
class HostApplication : Application() {
override fun onCreate() {
super.onCreate()
// Host app's Koin
startKoin {
androidContext(this@HostApplication)
modules(hostAppModule)
}
// SDK's isolated Koin
MySDK.initialize(SDKConfig(apiKey = "xxx"))
}
override fun onTerminate() {
super.onTerminate()
MySDK.shutdown()
}
}
// In Compose
@Composable
fun SDKFeature() {
KoinIsolatedContext(context = MySDK.koinContext) {
SDKScreen()
}
}
Accessing Both Contexts
Sometimes you need to access both host and SDK dependencies:
@Composable
fun BridgeScreen() {
// Get from host context (outside KoinIsolatedContext)
val hostAnalytics = koinInject<HostAnalytics>()
KoinIsolatedContext(context = MySDKKoinContext.koinApp) {
// Get from SDK context (inside KoinIsolatedContext)
val sdkService = koinInject<SDKService>()
// Use both
SDKFeature(
service = sdkService,
onEvent = { hostAnalytics.track(it) }
)
}
}
Complete SDK Example
// SDK public API
object PaymentSDK {
private lateinit var koinApp: KoinApplication
fun initialize(config: PaymentConfig) {
koinApp = koinApplication {
modules(
module { single { config } },
paymentCoreModule,
paymentUIModule
)
}
}
@Composable
fun PaymentScreen(
amount: Double,
onSuccess: (PaymentResult) -> Unit,
onCancel: () -> Unit
) {
KoinIsolatedContext(context = koinApp) {
PaymentFlow(
amount = amount,
onSuccess = onSuccess,
onCancel = onCancel
)
}
}
}
// SDK internal modules
private val paymentCoreModule = module {
single<PaymentProcessor>()
single<PaymentValidator>()
}
private val paymentUIModule = module {
viewModel<PaymentViewModel>()
}
// SDK internal UI
@Composable
private fun PaymentFlow(
amount: Double,
onSuccess: (PaymentResult) -> Unit,
onCancel: () -> Unit
) {
val viewModel = koinViewModel<PaymentViewModel>()
PaymentUI(
amount = amount,
state = viewModel.state,
onPay = { viewModel.processPayment(amount, onSuccess) },
onCancel = onCancel
)
}
// Host app usage
@Composable
fun CheckoutScreen() {
var showPayment by remember { mutableStateOf(false) }
if (showPayment) {
PaymentSDK.PaymentScreen(
amount = 99.99,
onSuccess = { result ->
showPayment = false
// Handle success
},
onCancel = { showPayment = false }
)
} else {
Button(onClick = { showPayment = true }) {
Text("Pay Now")
}
}
}
Best Practices
-
Initialize early - Set up isolated context before Compose renders
-
Clean up resources - Call
close()on the KoinApplication when done -
Don't share instances - Keep SDK and host dependencies separate
-
Use interface boundaries - Communicate via callbacks or interfaces, not shared Koin instances
-
Document initialization - Make SDK setup requirements clear to host app developers
Next Steps
- Compose Overview - Basic Compose setup
- Context Isolation - Core isolation concepts
- Scopes - Scope management in Compose