Post

Injecting Android Context in Compose Multiplatform with Koin

How to inject and use Android Context in a Compose Multiplatform app with Koin.

Injecting Android Context in Compose Multiplatform with Koin

While developing the Space Flight News app, I needed to inject the Android Context into Koin to set up SQLDelight and to trigger a native share intent.

There were quite a few examples of initializing Koin for every platform independently, using the initKoin function. However, I was using the KoinApplication composable function wrapper to initiate Koin from within the shared App composable function and found a lack of examples on how to do this.

This is a short article sharing how to set up your Compose Multiplatform app so that you can access the Android Context in your Koin modules when using the KoinApplication composable function wrapper.

Note: this article assumes the reader is familiar with Koin, Kotlin Multiplatform, and Compose Multiplatform. If you want to learn more, consider first reading Part 2 of Building a Space News App with Compose Multiplatform for Android, iOS, and Desktop.

❌ Wrong approach

One solution I saw recommended frequently was to create an empty Application class in the androidMain source set and store the context inside a static variable from onCreate() and access it within your Koin modules.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// composeApp/androidMain/MyApp.kt  
class MyApp: Application() {  
  
    override fun onCreate() {  
        super.onCreate()  
        androidContext = this  
    }  
}  
  
@SuppressLint("StaticFieldLeak")  
lateinit var androidContext: Context  
  
// composeApp/androidMain/AppModule.android.kt  
actual val platformModule: Module  
    get() = module {  
        // provide Context to Koin  
        single<Context> { androidContext }  
        // retrieve Context using get()  
        single<ShareService> { AndroidShareService(context = get()) }  
    }

While this might work, it’s not a scalable solution and can lead to bugs, crashes, and memory leaks.

✅ Option 1: Providing Context through optional KoinAppDeclaration

The option that I ended up using in my Space Flight News app is to add an optional KoinAppDeclaration argument to the main App composable function. This enables passing custom config to the Koin initialization that’s platform-specific. It’s only used by Android, while iOS and Desktop are not using it.

1
2
3
4
5
6
7
8
9
10
11
12
// composeApp/commonMain/App.kt  
@Composable  
fun App(koinAppDeclaration: KoinAppDeclaration? = null) {  
    KoinApplication(application = {  
        koinAppDeclaration?.invoke(this)  
        modules(appModule, platformModule)  
    }) {  
        AppTheme {  
            MainNavigationHost()  
        }  
    }  
}

Then in the MainActivity in androidMain, we’re building the KoinAppDeclaration instance and using the androidContext function from Koin to declare the specific instance of Context that Koin should use whenever we want to inject it.

1
2
3
4
5
6
7
8
9
10
11
12
13
// composeApp/androidMain/MainActivity.kt  
class MainActivity : ComponentActivity() {  
    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  
  
        setContent {  
            // provide KoinAppDeclaration  
            App({  
                androidContext(this@MainActivity.applicationContext)  
            })  
        }  
    }  
}

As Context is an Android-only concept; we can only access it within the androidMain source set, meaning that we must have platform-specific modules.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// composeApp/commonMain/AppModule.kt  
val appModule = module {  
    single<ApiService> { ApiService() }  
    single<ArticlesRepository> {  
        ArticlesRepository(  
            databaseDriverFactory = get(),  
            api = get()  
        )  
    }  
  
    viewModel { ArticleListViewModel(repository = get()) }  
}  
  
expect val platformModule: Module

Within the actual implementation of the platformModule in androidMain source set we can retrieve the Context using the standard get() function.

1
2
3
4
5
6
// composeApp/androidMain/AppModule.android.kt  
actual val platformModule: Module  
    get() = module {  
        single<DatabaseDriverFactory> { AndroidDatabaseDriverFactory(context = get()) }  
        single<ShareService> { AndroidShareService(context = get()) }  
    }

✅ Option 2: Using experimental KoinMultiplatformApplication in Koin 4.1.0

Release 4.1.0 of Koin added a new KoinMultiplatformApplication composable function that is the same as KoinApplication, but automatically injects the Android Context.

Note: the API is experimental and might change in future releases.

1
2
3
4
5
6
7
8
9
10
11
@OptIn(KoinExperimentalAPI::class)  
@Composable  
fun App() {  
    KoinMultiplatformApplication(config = koinConfiguration {  
        modules(appModule, platformModule)  
    }) {  
        AppTheme {  
            MainNavigationHost()  
        }  
    }  
}

Conclusion

We looked at a couple of options on how to inject and use Android Context within a Compose Multiplatform app when using Koin and KoinApplication composable function.

Hopefully, you found this helpful. Share your approach in the comments.


Resources:

This post is licensed under CC BY 4.0 by the author.