I’ve been using Koin in most of the Kotlin Multiplatform (KMP) samples I have but thought it would be good to include use of at least one other DI framework and this article outlines changes made to add kotlin-inject to the BikeShare KMP sample. This project retrieves data from a backend using Ktor and stores data locally on the device using Realm. It supports several platforms but we’ll be focusing on Android and iOS here.

Common KMP code

Starting off, the following were the changes made to libs.version.toml and build.gradle.kts. Note also that we’re setting the generateCompanionExtensionsoption…this allows a more convenient way of creating the generated kotlin-inject components.

libs.version.toml

1
2
3
4
5
kotlininject = "0.7.1"


kotlininject-compiler = { module = "me.tatarka.inject:kotlin-inject-compiler-ksp", version.ref = "kotlininject" }
kotlininject-runtime = { module = "me.tatarka.inject:kotlin-inject-runtime", version.ref = "kotlininject" }

build.gradle.kts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
commonMain.dependencies {
    ...

    implementation(libs.kotlininject.runtime)
}

...

ksp {
    arg("me.tatarka.inject.generateCompanionExtensions", "true")
}

dependencies {
    add("kspAndroid", libs.kotlininject.compiler)
    add("kspIosX64", libs.kotlininject.compiler)
    add("kspIosArm64", libs.kotlininject.compiler)
    add("kspIosSimulatorArm64", libs.kotlininject.compiler)
    add("kspJvm", libs.kotlininject.compiler)
}

We then define the shared dependencies in SharedApplicationComponent.kt as shown below. We’re using a single interface given the scope of this project but typically, in larger projects, the dependencies would get grouped into different interfaces.

As we’ll see later, we’re also going to create platform specific components that subclass this interface. This will allow both creating platform specific versions of the dependencies (for example getHttpClientEnginewhich is used to create Ktor client engine for each platform) and also to allow clients to pass in objects that might be needed for each platform (though not doing that yet in this project).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
interface SharedApplicationComponent {
    val countriesViewModel: CountriesViewModelShared
    val networksViewModel: NetworksViewModelShared
    val stationsViewModel: StationsViewModelShared


    val repository: CityBikesRepository
    val cityBikesApi: CityBikesApi

    val json: Json
        @Provides get() = Json { isLenient = true; ignoreUnknownKeys = true; useAlternativeNames = false }

    val realm: Realm
        @Provides get() {
            val config = RealmConfiguration.create(schema = setOf(NetworkDb::class))
            return Realm.open(config)
        }

    @Provides
    fun getHttpClientEngine(): HttpClientEngine

    @Provides
    fun httpClient(): HttpClient = createHttpClient(getHttpClientEngine(), json)
}

fun createHttpClient(httpClientEngine: HttpClientEngine, json: Json) = HttpClient(httpClientEngine) {
    install(ContentNegotiation) {
        json(json)
    }
    install(Logging) {
        logger = Logger.DEFAULT
        level = LogLevel.INFO
    }
}

The way those dependencies get injected then in various classes is through use of @Inject annotation as shown here (we also mark in this case as @Singleton)

1
2
3
4
@Inject @Singleton
class CityBikesRepository(val cityBikesApi: CityBikesApi,val  realm: Realm) {
    ...
}


Android

The Android specific version of the kotlin-inject component (AndroidApplicationComponent.kt) is shown below (created in androidMain in the shared KMP module). In this case we’re using that component to add dependencies for various Composable functions that we want to be passed particular dependencies. We also define the Android specific version of the Ktor client engine here.

1
2
3
4
5
6
7
8
9
10
11
@Component
@Singleton
abstract class AndroidApplicationComponent: SharedApplicationComponent {
    abstract val countryListScreen: CountryListScreen
    abstract val networkListScreen: NetworkListScreen
    abstract val stationsScreen: StationsScreen

    override fun getHttpClientEngine() = Android.create()

    companion object
}

To allow dependencies to be passed to Composable functions we need to use approach shown below (more info on that in this page). Note also the use of @Assisted for parameters that we’ll be using when those composable functions are used.

1
2
3
4
5
6
7
8
typealias CountryListScreen = @Composable ((country: Country) -> Unit) -> Unit

@Inject
@Composable
fun CountryListScreen(viewModel: CountriesViewModelShared, @Assisted countrySelected: (country: Country) -> Unit) {
    val countryList by viewModel.countryList.collectAsState()
    ...
}

The following are the changes needed then in the Android client module. We firstly add code to create the component in BikeShareApplication.kt

1
2
3
4
5
6
7
class BikeShareApplication : Application() {
    val component: AndroidApplicationComponent by lazy {
        AndroidApplicationComponent.create()
    }

    ...    
}

That gets used then in MainActivity.kt as shown below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        val applicationComponent = (applicationContext as BikeShareApplication).component

        setContent {
            BikeShareTheme {
                BikeShareApp(applicationComponent)
            }
        }
    }
}

The android app is using Jetpack Navigation and this is an example of how the appropriate Composable function was retrieved from the component and used then in our NavHost.

1
2
3
4
5
6
7
8
9
10
NavHost(navController, startDestination = Screen.CountryListScreen.title) {
    val countryListScreen = applicationComponent.countryListScreen
    composable(Screen.CountryListScreen.title) {
        countryListScreen {
            navController.navigate(Screen.NetworkListScreen.title + "/${it.code}")
        }
    }

    ...
}


iOS

For iOS we also have a platform specific component (IosApplicationComponent as shown below) which is created in iosMain in the shared KMP module. As with Android, we’re also creating the platform specific version of the Ktor client engine here.

1
2
3
4
5
6
7
8
@Component
@Singleton
abstract class IosApplicationComponent: SharedApplicationComponent {

    override fun getHttpClientEngine() = Darwin.create()

    companion object
}

In our SwiftUI client we create that component on startup and pass to ContentView.

1
2
let applicationCompoonent = IosApplicationComponent.companion.create()
let contentView = ContentView(applicationCompoonent: applicationCompoonent)

The following then shows example in ContentView of how we’re using that component to look up one of the shared view models (done in this way also to allow StateFlows in the shared view models to be observed in SwiftUI….using KMP-ObservableViewModel library).

1
2
3
4
5
6
7
8
9
10
11
12
13
struct ContentView : View {
    let applicationCompoonent: IosApplicationComponent
    @ObservedViewModel var viewModel: CountriesViewModelShared
    
    init(applicationCompoonent: IosApplicationComponent) {
        self.applicationCompoonent = applicationCompoonent
        self.viewModel = applicationCompoonent.countriesViewModel
    }
    
    var body: some View {
        ...
    }
}


Featured in Android Weekly #633 and Kotlin Weekly Issue #418