Implementing Dependency Injection in Compose Multiplatform Projects with Koin Annotations
Koin Multiplatform Mobile

Implementing Dependency Injection in Compose Multiplatform Projects with Koin Annotations

Koin is a dependency injection framework. It helps developers with dependency injection during development. For example, when we use a repository in a ViewModel, we should use Koin to inject the dependency as a singleton object. This ensures that the object is not created repeatedly, but rather instantiated once and then reused whenever we call the ViewModel in the project.

What is Koin Annotation? Koin annotation allows using Koin with annotations, such as:

@Module
@ComponentScan("org.koin.sample")
class AppModule 

@Single
class UserRepositoryImpl : UserRepository         

  • We use the @Module keyword to define AppModule as a module.
  • To create the UserRepositoryImpl class as a singleton we use with @Single

Let's we create a simple real-world example:

Firstly, let me share the project structure:

UserKMMApp/
├── androidApp/
│   ├── build.gradle.kts
│   ├── src/main/
│   │   ├── AndroidManifest.xml
│   │   ├── kotlin/com/example/userkmmapp/android/MainActivity.kt
├── iosApp/
│   ├── Podfile
│   ├── iosApp.xcodeproj
│   ├── iosApp/
│   │   ├── ContentView.swift
│   │   ├── iOSApp.swift
├── shared/
│   ├── build.gradle.kts
│   ├── src/commonMain/
│   │   ├── kotlin/com/example/userkmmapp/Repository.kt
│   │   ├── kotlin/com/example/userkmmapp/UserViewModel.kt
│   │   ├── kotlin/com/example/userkmmapp/Modules.kt
│   │   ├── kotlin/com/example/userkmmapp/ui/UserScreen.kt
│   │   ├── kotlin/com/example/userkmmapp/ui/Platform.kt
│   │   ├── kotlin/com/example/userkmmapp/ui/Theme.kt
│   ├── src/androidMain/
│   │   ├── kotlin/com/example/userkmmapp/android/UserApplication.kt
│   ├── src/iosMain/
│   │   ├── kotlin/com/example/userkmmapp/InitKoin.kt
├── build.gradle.kts
├── settings.gradle.kts        

Secondly, let's add the dependencies for the project:

build.gradle.kts(:Root):

plugins {
    // Add the needed dependency
    id("io.insert-koin") version "3.1.4" apply false   
}

allprojects {
    repositories {
        google()
        mavenCentral()
    }
}}        

We added the Koin dependency to the Root build.gradle

shared/build.gradle.kts:

plugins {
    id("io.insert-koin")
}

kotlin {
 //...
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("io.insert-koin:koin-core:3.2.0")
                implementation("io.insert-koin:koin-annotations:3.1.4")
            }
        }
        val androidMain by getting {
            dependencies {
                implementation("io.insert-koin:koin-android:3.2.0")
                kapt("io.insert-koin:koin-compiler:3.1.4")
            }
        }
        val iosMain by getting {
            dependencies {
                implementation("io.insert-koin:koin-core:3.2.0")
            }
        }
    }
}        

Thirdly, we added the dependencies to shared/build.gradle.kts file. In commonMain, we added Koin core and Koin annotations dependencies. In androidMain & iosMain, we added the platform-specific codes.

In the common file, we create the Repository file:

shared/src/commonMain/kotlin/com/example/userkmmapp/Repository.kt:

package com.example.userkmmapp

import org.koin.core.annotation.Single

interface UserRepository {
    fun getUserData(): String
}

@Single
class UserRepositoryImpl : UserRepository {
    override fun getUserData(): String = "Hi user, Koin with Compose Multiplatform!"
}        

Here, the Repository is implemented as UserRepositoryImpl class and that dependency is annotated with @Single keyword to create the UserRepositoryImpl object once as a singleton.

Fifth, we inject the Repository as a constructor dependency into UserViewModel:

shared/src/commonMain/kotlin/com/example/userkmmapp/UserViewModel.kt:

package com.example.userkmmapp

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.koin.core.annotation.Factory

@Factory
class UserViewModel(private val userRepository: UserRepository) {
    private val _state = MutableStateFlow(userRepository.getUserData())
    val state: StateFlow<String> = _state
}        

In UserViewModel, we add UserRepository as a constructor dependency and annotate the class with @Factory.

Sixth, we create a UserAppModule class to define the module in the shared module:

shared/src/commonMain/kotlin/com/example/userkmmapp/Modules.kt:

package com.example.userkmmapp

import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Module

@Module
@ComponentScan
class UserAppModule        

Seventh, we create the UserScreen composable class for the App UI and declare it in the commonMain module of the shared module:

shared/src/commonMain/kotlin/com/example/userkmmapp/ui/UserScreen.kt:

package com.example.userkmmapp.ui

import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import com.example.userkmmapp.UserViewModel
import org.koin.compose.koinInject

@Composable
fun UserScreen(userViewModel: UserViewModel = koinInject()) {
    val userState by userViewModel.state.collectAsState()

    PlatformText(text = userState) // Call the platform-specific function in commonMain
}        

Here, we inject UserViewModel with a dependency using the default koinInject() assignment.

Eighth, we start Koin in androidMain & iosMain. First, we start with Android:

shared/src/androidMain/kotlin/com/example/userkmmapp/android/UserApplication.kt:

package com.example.userkmmapp.android

import android.app.Application
import com.example.userkmmapp.UserAppModule
import org.koin.core.context.startKoin
import org.koin.ksp.generated.module

class UserApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            modules(UserAppModule().module)
        }
    }
}        

Starting Koin with this call: modules(UserAppModule().module)

Ninth, we call the shared function for platform-specific functionality and implement the actual function to inform the shared module's commonMain module:

shared/src/androidMain/kotlin/com/example/userkmmapp/ui/Platform.kt:

package com.example.userkmmapp.ui

import androidx.compose.material.Text
import androidx.compose.runtime.Composable

@Composable
actual fun PlatformText(text: String) {
    Text(text = text)
}
        

Tenth, we use the UserAppTheme composable function for the Android and iOS UIs in AndroidApp and iOSApp:

shared/src/commonMain/kotlin/com/example/userkmmapp/ui/Theme.kt:

import androidx.compose.runtime.Composable

@Composable
fun UserAppTheme(content: @Composable () -> Unit) {
    content()
}        

We will use the UserAppTheme composable to call the UI of commonMain in AndroidApp and iOSApp.

Let's use the UserAppTheme in MainActivity:

androidApp/src/main/kotlin/com/example/userkmmapp/android/MainActivity.kt:

package com.example.userkmmapp.android

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.example.userkmmapp.ui.UserAppTheme
import com.example.userkmmapp.ui.UserScreen

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            UserAppTheme {
                UserScreen()
            }
        }
    }
}        

As you can see, we use the UserScreen() in the UserAppTheme composable. UserScreen() is taken from the shared module's commonMain.

Let's add android:name=".UserApplication" to the AndroidManifest for naming the Koin dependency injection:

androidApp/src/main/AndroidManifest.xml:

<manifest xmlns:android="https://schemas.android.com/apk/res/android"
    package="com.example.userkmmapp.android">
    <application
        android:name=".UserApplication"
        <!-- Other configurations -->
    </application>
</manifest>        

For iOS Code:

shared/src/iosMain/kotlin/com/example/userkmmapp/InitKoin.kt:

package com.example.userkmmapp

import org.koin.core.context.startKoin
import org.koin.ksp.generated.module

fun initKoin() {
    startKoin {
        modules(UserAppModule().module)
    }
}        

Starting Koin in iosMain with initKoin function.

Next, we will use the platform-specific code in iOS App:

shared/src/iosMain/kotlin/com/example/userkmmapp/ui/Platform.kt:

package com.example.userkmmapp.ui

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.ComposeUIViewController

fun MainViewController() = ComposeUIViewController {
    UserAppTheme {
        UserScreen()
    }
}

@Composable
actual fun PlatformText(text: String) {
    Box(
        modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center)
    ) {
        Text(
            text = text,
            fontSize = 24.sp,
            fontWeight = FontWeight.Bold,
            color = Color.Black
        )
    }
}        

We wrote the PlatformText with actual to take shared code from commonMain. ComposeUIViewController is a helper for writing compose code for UI in iOSApp.

We will use the iOSApp file:

iosApp/iOSApp.swift:

import SwiftUI
import shared

@main
struct iOSApp: App {
    init() {
        KoinKt.initKoin()
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    var body: some View {
        ComposeView()
    }
}

struct ComposeView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        MainViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}        

We make UIViewControllerRepresentable for the ComposeUIViewController code here, and we do the function calls.

iosApp/ContentView.swift:

import SwiftUI

struct ContentView: View {
    var body: some View {
        ComposeView()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}        

Inside ContentView, we call ComposeView(), and we use ContentView_Previews for iOS device preview.


In this guide, we explored how to implement dependency injection in a Kotlin Multiplatform project using Koin Annotations and Compose Multiplatform. By setting up Koin in both Android and iOS environments, we demonstrated how to efficiently manage dependencies across platforms. With the help of Koin Annotations, we streamlined the creation and injection of singleton and factory instances, enabling a more modular and maintainable codebase. This approach not only simplifies code maintenance but also ensures a consistent and efficient dependency management strategy, empowering developers to build robust, scalable, and high-performance applications across multiple platforms.


Happy Coding. ??


Medium: https://medium.com/@ahmetbostanciklioglu

LinkedIn: https://www.dhirubhai.net/in/ahmetbostanciklioglu/

GitHub: https://github.com/ahmetbostanciklioglu

要查看或添加评论,请登录

Ahmet Bostanciklioglu的更多文章

其他会员也浏览了