Getting Started with Compose Multiplatform and Koin: A Practical Guide

Muhammad Khubaib Imtiaz
6 min readJun 5, 2024

--

Compose Multiplatform is an amazing framework built by JetBrains to create cross-platform applications using Kotlin. In today’s article, I’ll dive into a detailed practical guide for creating Compose Multiplatform Applications with Koin as Dependency Injection from scratch. So, let’s get started.

What is Compose Multiplatform?

Compose Multiplatform is a cross-platform framework developed by JetBrains. In Compose Multiplatform, we share the logic and the UI part too. We can create Android, iOS, Web, and Desktop Applications with Compose Multiplatform. Here are some existing projects that are developed using Compose Multiplatform and Koin as Dependency Injection, and I'll use this YouTube-Clone-KMP as a demo tutorial.

Benefits of using Compose Multiplatform for cross-platform development.

There are several benefits that Compose Multiplatform projects provide us. First of all, it provides seamless integration. We can easily integrate new features, upgrade an existing project to Compose Multiplatform, and it also enhances productivity. Moreover, it provides a modern UI toolkit (Jetpack Compose), consistency, and code reusability. The main thing that I like the most about Compose Multiplatform is the ability to share native code. This is the main difference compared to other cross-platform frameworks.

Prerequisites:

You need to set up the environment before moving forward. Here is the detailed documentation that you can follow: Set up an environment | Kotlin Multiplatform Development.

Project Setup:

After setting all the environment, We need to create a new project from the Kotlin Multiplatform Wizard or Compose Multiplatform Wizard. In my YouTube Clone KMP, I used the Compose Multiplatform Wizard. There’s only one different between them. The KMP Wizard doesn’t provide any dependencies but in Compose Multiplatform you can add them easily on the wizard page.

Adding Dependencies:

After creating the Project, You need to add all the required dependencies in the gradle/libs.versions.toml.
Required Dependencies:

[versions]

ktor = "2.3.11"
koin-bom = "3.5.6"
kotlinx-serialization = "1.6.3"
kamelImage = "0.9.4"


[libraries]

ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }

//Koin
koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" }
koin-core = { module = "io.insert-koin:koin-core" }

// Or Use this directly inside gradle.build

// CommonMain Module
implementation(project.dependencies.platform("io.insert-koin:koin-bom:3.5.4"))
implementation("io.insert-koin:koin-core")
implementation("io.insert-koin:koin-compose")

// Android Module
implementation(project.dependencies.platform("io.insert-koin:koin-bom:3.5.4"))
implementation("io.insert-koin:koin-core")
implementation("io.insert-koin:koin-android")

Setting Up Koin:

After adding all the dependencies, we need to set up Koin for the project. Koin can be set up in different ways. I’ll try to cover a few of them. Create a di package inside the commonMain module and create the module for Koin.

val appModule = module {
single {
HttpClient {
install(ContentNegotiation) {
json(
Json {
isLenient = true
explicitNulls = false
ignoreUnknownKeys = true
},
contentType = ContentType.Application.Json
)
}

install(Logging) {
level = LogLevel.ALL
logger = object : Logger {
override fun log(message: String) {
println(message)
}

}
}

install(HttpTimeout) {
connectTimeoutMillis = Constant.TIMEOUT
requestTimeoutMillis = Constant.TIMEOUT
socketTimeoutMillis = Constant.TIMEOUT
}
}
}
single{ YouTubeServiceImpl() }
single { YoutubeDatabase(DriverFactory().createDriver()) }
singleOf(::MainViewModel)
}

In the appModule, I provide all of my dependencies that are going to be required in any class or a composable. You can design it according to your needs. We can also use the factory function where we want to provide a different instance each time.

Most Liked Featured of Koin (Recommended by me):

There are different features that I like about Koin, but my most favorite one is singleOf. Let me explain why. Suppose if you want to provide 10 dependencies to MainViewModel, you need to write code like this:

val appModule = module {
single {(MainViewModel(get(),get(),get(),get()} // upto 10-15
}

And we need to provide the qualifier too to tell the compose to provide what MainViewModel requires. To handle this, singleOf is a really amazing and lovable feature. Where we just need to define any dependency with the singleOf keyword and it'll do all the magic by itself. In the above example, as we provided each dependency separately, we didn't need to do that. Koin will check out and provide everything by itself.

Here is MainViewModel:

@KoinViewModel
class MainViewModel(
private val repository: YouTubeServiceImpl,
private val database: YoutubeDatabase,
) : ViewModel() {
private val _videos = MutableStateFlow<ResultState<Youtube>>(ResultState.LOADING)
val videos: StateFlow<ResultState<Youtube>> = _videos.asStateFlow()

private val _videosUsingIds = MutableStateFlow<ResultState<Youtube>>(ResultState.LOADING)
val videosUsingIds: StateFlow<ResultState<Youtube>> = _videosUsingIds.asStateFlow()

private val _relevance = MutableStateFlow<ResultState<Youtube>>(ResultState.LOADING)
val relevance: StateFlow<ResultState<Youtube>> = _relevance.asStateFlow()

fun getVideosList(userRegion: String) {
viewModelScope.launch {
_videos.value = ResultState.LOADING
try {
val response = repository.getVideoList(userRegion)
_videos.value = ResultState.SUCCESS(response)
} catch (e: Exception) {
val error = e.message.toString()
_videos.value = ResultState.ERROR(error)
}

}
}
fun getVideosUsingIds(ids: String) {
viewModelScope.launch {
_videosUsingIds.value = ResultState.LOADING
try {
val response = repository.getVideosUsingIds(ids)
_videosUsingIds.value = ResultState.SUCCESS(response)
} catch (e: Exception) {
val error = e.message.toString()
_videosUsingIds.value = ResultState.ERROR(error)
}

}
}

fun getRelevance() {
viewModelScope.launch {
_relevance.value = ResultState.LOADING
try {
val response = repository.getRelevance()
_relevance.value = ResultState.SUCCESS(response)
} catch (e: Exception) {
val error = e.message.toString()
_relevance.value = ResultState.ERROR(error)
}

}
}
}

Now, Koin will automatically provide the dependencies where we use them. We haven’t completed the Koin setup. Let’s move forward with the next step.

Koin Application:

There are different ways to provide a dependency module in Compose Multiplatform and the Koin Application is one of them. By using Koin Application, we just need to provide all the UI content inside the Koin Application and provide appModule inside it as a modifier.

// Path composeApp/src/commonMain/kotlin/org/company/app/App.kt

@Composable
internal fun App() = AppTheme {
KoinApplication(
application = {
modules(appModule)
}
) {
AppContent()
}
}

The Koin Application is a really simple way to provide a module for each platform. But we can also provide modules on each platform too.

Android Module:

// androidMain
class AndroidApp : Application() {
companion object {
lateinit var INSTANCE: AndroidApp
}

override fun onCreate() {
super.onCreate()
INSTANCE = this
startKoin {
androidContext(this@AndroidApp)
androidLogger()
modules(appModule)
}
}
}

You can start Koin inside the androidMain module by writing the above code inside the onCreate() method.

iOS Module:

// create this function inside the : composeApp/src/iosMain/kotlin/org/company/app/App.ios.kt
fun initKoin(){
startKoin {
modules(appModule)
}
}

// provide module here: iosApp/iosApp/iosApp.swift
@main
struct iosApp: App {
init(){
MainKt.doInitKoin()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

JvmMain Module:

@OptIn(ExperimentalResourceApi::class)
fun main() = application {
startKoin {
modules(appModule)
}
Window(
title = "Youtube Clone",
icon = painterResource(Res.drawable.youtube_music),
state = rememberWindowState(width = 1280.dp, height = 720.dp),
onCloseRequest = ::exitApplication,
) {
window.minimumSize = Dimension(350, 600)
App()
}
}

jsMain Module:

@OptIn(ExperimentalComposeUiApi::class)
fun main() {
startKoin {
modules(appModule)
}
onWasmReady {
CanvasBasedWindow("Youtube Clone") {
App()
}
}
}

This is how we can provide dependencies with Koin. Here is the demo of the YouTube Clone using Compose Multiplatform with Koin as Dependency Injection.

Demo :

YouTube Clone KMP

Conclusion:

In this article, we’ve explored the powerful combination of Compose Multiplatform and Koin for creating cross-platform applications. Compose Multiplatform allows for shared logic and UI across Android, iOS, Web, and Desktop, enhancing code reusability and consistency. Koin simplifies dependency injection with features like singleOf, making it easier to manage dependencies. By following our setup and example of the YouTube Clone KMP, you can see how these tools streamline development and improve productivity. With Compose Multiplatform and Koin, you can build robust, high-quality applications efficiently.

Link:
Compose Multiplatform Wizard: https://terrakok.github.io/Compose-Multiplatform-Wizard/

Kotlin Multipaltform Wizard: https://kmp.jetbrains.com/

Koin Setup Official Documentation: https://insert-koin.io/docs/setup/koin

YouTube Clone KMP: https://github.com/KhubaibKhan4/Youtube-Clone-KMP/

--

--

Muhammad Khubaib Imtiaz

Software Engineer Android | Kotlin Mutlipaltform | Compose Multiplatform. Community Lead @Kotzilla