Blog Infos
Author
Published
Topics
, , , ,
Published

With the rise of Kotlin Multiplatform (KMP), it gave birth to a cross-platform UI library, Compose Multiplatform (CMP). CMP is currently quite fresh out in the market. At the time of writing this article, it is still beta for iOS applications and alpha for the web (mainly because Kotlin/Wasm is still alpha).

https://www.jetbrains.com/lp/compose-multiplatform/

While working on a current project in CMP, I researched and experiment with various Dependency Injection (and Service locator) libraries and kodein-di stood out to me as a clear winner when it comes to their integration in CMP projects. I choose this library because:

  • Easy dependencies — Unlike other libraries, the setup was simple as installing a single dependency that worked in all aspects.
  • Great documentation — Although some sections of the documentation were confusing, my basic needs were described in detail.
  • Simple constructs — The process of defining and retrieving dependencies had a natural flow that Dagger Hilt provided during native development.
Photo by Thomas Couillard on Unsplash

 

The context

A project with CMP changes how a KMP project is structured. And for those who don’t understand dependency injection completely, I have a couple important definitions that you should be familiar with.

Folder structure
|- composeApp/ # This is UI module for sharing UI code between platforms
|  |- src/
|  |- build.gradle.kts
|- gradle/
|- iosApp/
|- server/
|- shared/ # Shared business logic (KMP)
|  |- src/
|  |- build.gradle.kts
Bindings

Bindings can be understood as a mapping of a type to the actual object. E.g. I could bind all Strings to "Look dad, DI!".

DI Container/Module/Instance

The documentation on kodein-di uses these terms interchangeably. They are a collection of bindings. Later you would see that we can export and extend DI modules with kodein-di and use it similar to Object-Oriented Programming. Would this be called “Module-Object Programming”? 🤔

Singletons

Within a DI container, requesting an instance will always return the same object. Singletons are lazy so they would never be created if the application never asked for it.

Providers & Factories

Within a DI container, every time you request for an instance of a type, it will return a new object. Providers don’t accept any parameters and their function signature is () -> T. Factories can pass in a single parameter to create a dependency on every instance request. They have a function signature of (A) -> T.

Dependency Injection in CMP Project

If you don’t understand the previous definitions fully, you might understand them with some code samples. First, we should implement the dependency.

Installing Kodein-DI for Compose

In your gradle/libs.versions.toml,

[versions]
kodeinDiCompose = "7.22.0"

[libraries]
kodein-di-compose = { module = "org.kodein.di:kodein-di-framework-compose", version.ref = "kodeinDiCompose" }

And you would require to add the dependency in the commonMain sourceSet of composeApp and shared modules.

// In both composeApp/build.gradle.kts
// and shared/build.gradle.kts

kotlin {
  sourceSets {
    commonMain.dependencies {
      implementation(libs.kodein.di.compose)
    }
  }
}

Sync the project and allow Gradle to download the repository from upstream remote.

Create your App-level Module

Coming from a background of using Dagger Hilt in native android development, I like having a global dependency container to hoist dependencies the entire app should use.

Create a file shared/src/commonMain/kotlin/AppModule.kt and add the following code:

import org.kodein.di.DI

val appModule = DI {}

Invoking the DI function creates a DI container to work in. To add a binding to this container, we use the bind function supplying the type I want to bind it to:

val appModule = DI {
  bind<Repository> { /* see below */ }
}

// Assume that this exists:
interface Repository

Within the lambda function, you have to specify the type of binding you want to provide i.e. a singleton, provider or factory. For the sake of demonstration, let’s provide a singleton:

val appModule = DI {
  bind<Repository> { singleton { RepositoryImpl() } }
}

// Assume that this exists:
interface Repository
class RepositoryImpl : Repository

Awesome! We created our first dependency inside a DI container. You shorten the bind { singleton {} } line to bindSingleton {}:

val appModule = DI {
  bindSingleton<Repository> { RepositoryImpl() }
}

// And other functions include:
val appModule = DI {
  bindSingleton<Repository> { RepositoryImpl() }
  bindProvider<Repository> { ReposioryImpl() }
  bindFactory<Params, Repository> { params -> RepositoryImpl(params) }
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

Using dependencies in CMP

Oftentimes, you would have a ViewModel associated with a screen drawn with Compose. Previously, you write some code such as:

@Composable
fun HomeScreen() {
  val viewModel = viewModel { HomeViewModel(RepositoryImpl()) }

  Text(text = viewModel.text)
}

// Assume that this exists:
class HomeViewModel(
  private val repository: Repository
) : ViewModel()

With kodein-di-framework-compose, dealing with Dependency Injection within composable functions is easy.

import org.kodein.di.compose.rememberInstance
import org.kodein.di.compose.subDI

@Composable
fun HomeScreen() {
  subDI(
    parentDi = appModule // from AppModule.kt above
    diBuilder = {
      bindSingleton { HomeViewModel(instance()) }
    }
  ) {
    val viewModel by rememberInstance<HomeViewModel>()

    Text(text = viewModel.text)
  }
}

// Assume that this exists:
class HomeViewModel(
  private val repository: Repository
) : ViewModel()

Breaking down the code happening above:

  1. subDI is a function that extends any local DI container for the given Composable function. Here, we extend from appModule
  2. diBuilder is a lambda function supplied to subDI that attaches any additional local binding to the container.
  3. instance is where we are retrieving the Repository type. But how is that happening? We know that HomeViewModel requires a type of Repository. So the function instance inferred that Repository is needed to be returned. instance looks for a binding of Repository in the container. The container extends appModule where the binding is defined. It returns the dependency to HomeViewModel and binds it.
  4. rememberInstance grabs the instance we are looking for inside the Composable, explicitly supplying the type required.
Conclusion

Et Voilà! You have implemented Dependency Injection in one of the simplest way. There are many features that kodein-di and kodein-di-framework-compose has to offer. Their documentation is fantastic so do check it out:

Getting started with Kodein-DI
Edit description

kosi-libs.org

Kodein-DI and Compose (Android, Desktop, or JS)
You can use as-is in your Android / Desktop / JS project, but you can level-up your game by using the library…

kosi-libs.org

Thank you for reading if you made it this far. Consider following me to read more articles from me regarding Kotlin, Nuxt3 and general programming practices.

Want to connect?

GitHub profile
Portfolio website

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
Menu