Serengeti logo BLACK white bg w slogan
Menu

Simplifying Dependency Injection in KMM with Koin

Hamza Bin Tariq. Senior Software Developer
16.03.2023.

This is a follow-up to our previous article about Getting Started with KMM, so if you haven’t read that, I would recommend checking it out first to get an idea of how things work in KMM.

If you are here, then I will assume that you already have an idea of dependency injection and the koin. If not, you can check this article to get an idea of how things work with koin in android. Now, let’s get to implementing this into KMM.

Koin provides us with an all-in kotlin library to use it in our shared module to create injections that can be used by both Android and iOS platforms, and also with desktop, if you go with KMP (Kotlin MultiPlatform). But, before we start getting into the coding implementation, there are two things to remember:
1. If you are making an injection of a class that is purely in a shared module and does not have anything to do with the platforms, you should make it in the common_main. For example, any helper class.

2. If you are making any injection of class that depends on the platform, like ViewModel, etc, you have to create the injections in andorid_main & ios_main using the expect & actual pattern.

And trust me, it is easier and simpler than you think.

Along with the Koin, we will be using the moko-mvvm library for creating ViewModel, which is also an all-in kotlin library that helps us in creating Viewmodels and Dispatchers that can be used by both android and iOS.
I will break this into three sections, shared , android & iOS .

Shared

Let’s add the required dependency first in build.gradle.kts (shared).

val commonMain by getting {
    dependencies {
        implementation("io.insert-koin:koin-core:3.3.3")
        api("dev.icerock.moko:mvvm-core:0.13.1")
    }
}

We have added both libraries for koin and moko-mvvm. These are enough to get us going to create ViewModels and injection. But there is another plugin by Koin that helps us make things easier with jetpack compose.

val androidMain by getting {
            dependencies {
                implementation("io.insert-koin:koin-androidx-compose:3.4.2")
            }
        }

This library helps us to get rid of extra code on jetpack compose and use injections in a very simple manner which I will demonstrate in the android section. You have to add this dependency in android level gradle as well.

implementation("io.insert-koin:koin-androidx-compose:3.4.2")

That’s it for libraries. Now let’s get to creating classes that we will be injecting.

class SampleUseCase {
    fun getString(): String = "This is the string coming from usecase in shared module."
}

class SampleViewModel(private val useCase: SampleUseCase) : ViewModel() {
    fun getStringFromUseCase(): String = useCase.getString()
}

Here we have a use-case that will be injected in the ViewModel and the ViewModel that we will be injecting in the android and iOS platforms.

Now, here SampleUseCase is entirely for the shared module, so we can create its injection in common_main, but SampleViewModel is using ViewModel() by moko-mvvm, which on the backend uses expect & actual pattern to provide the correct implementation for ViewModel from both android and iOS. As Android has native ViewModels and iOS doesn’t, it provides the correct implementation for it in actual promise. So, we will need to create ViewModels injection separately on both android_main & ios_main.

To do this, let’s create expect in our Platform.kt file that will require injection Module from both platforms.

expect fun getPlatform(): Module

Now, let’s create a file named KoinModule.kt and create a function in it.

import org.koin.core.context.startKoin
import org.koin.dsl.module

fun initKoin() = startKoin {
    modules(
        module {
            single {
                SampleUseCase()
            }
               },
        getPlatform()
    )
}

Here we are initialising Koin and providing it with the required modules using single to create a single instance of SampleUseCase and also adding in the platform-specific modules that we will get from android_main & ios_main in the actual of the getPlatform() .

Now, in the Platform.kt in android_main, let’s create an actual for getPlatform().

import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module

actual fun getPlatform() = module {
    viewModel {
        SampleViewModel(get())
    }

}

Here we are utilizing the koin-compose plugin to create the injection for ViewModel as native. And get() is used to find and inject the required dependency in this ViewModel before making its injection. In this case, it will be singleton of SampleUseCase that we have created in common_main. If you don’t create all of the required dependencies, koin will not be initialized and will crash at build time.

Now, let’s do the same in the Platform.kt in ios_main.

import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.dsl.module

actual fun getPlatform() = module {
   single{
       SampleViewModel(get())
   }
}
object GetViewModels: KoinComponent {
    fun getSampleViewModel() = get<SampleViewModel>()
}

Since iOS does not have ViewModels, we are using the singleton pattern. And here we also have created the helper object GetViewModels that will provide us with the instance of the required ViewModel. We don’t need it on the android side because of koin-compose.

That’s it for the shared module.  Let’s move to the platforms.

Android

In Android, first we have to call the initKoin() method from our application to start the koin.

import android.app.Application
import com.example.samplekmmproject.initKoin

class SampleApplication: Application() {
    override fun onCreate() {
        super.onCreate()
        initKoin()
    }
}

And now we can use injections in our composables pretty easily.

@Composable
fun DefaultTextView(viewModel: SampleViewModel = getViewModel()) {
  Text(text = viewModel.getStringFromUseCase())
}

class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MyApplicationTheme {
        Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
          DefaultTextView()
        }
      }
    }
  }
}

That’s all there is to do. getViewModel() will find and inject the required viewmodel if there is any, and will throw an exception if you haven’t created the required injection.

iOS

On the iOS side, we get to make some tweaking to make things work more smoothly. But don’t worry, these are very simple and easy steps. Let’s get to it.

First of all, you will have to rebuild the project in your Xcode to get all the changes you made in the shared module.

Now, first of all, just as we did in the Application class in android, iOS has the iOSApp class as its entry point. That is where we will have to start the Koin.

import SwiftUI
import shared

@main
struct iOSApp: App {
   
    init() {
        KoinModuleKt.doInitKoin()
    }
   
 var body: some Scene {
  WindowGroup {
   ContentView()
  }
 }
}

Don’t worry about the names. Upon compilation from Koltin to Swift, it merges the file extension with its name and changes the helper function names a little bit as well.

Now, in iOS we have ObservableObject instead of ViewModels, so we have to convert our ViewModel to make it usable in our UI.

import Foundation
import shared

public class SampleObservableObject: ObservableObject {
    var viewModel: SampleViewModel
   
    init(wrapper: SampleViewModel) {
        viewModel = wrapper
    }
   
}
public extension  SampleViewModel {
    func asObservableObject() -> SampleObservableObject {
        return  SampleObservableObject(wrapper: self)
    }
}

Here we have created an ObservableObject and an extension function of SampleViewModel that will return as SampleObservableObject.

Now in our view.

import SwiftUI
import shared

struct ContentView: View {
    @ObservedObject var viewModel = GetViewModels().getSampleViewModel().asObservableObject()
   
 var body: some View {
        Text(viewModel.viewModel.getStringFromUseCase())
 }
}

Here we are using the GetViewModels() object we created in ios_main to get the singleton instance of SampleViewModel and then also to convert it into an ObservedObject using the extension function we created.

And that is it for both sides. Now run the applications on both of the platforms and you will get the same string we created in SampleUseCase, like this:

image 24

And that's it for Dependency Injection in KMM. Easy, right?

Let's do business

The project was co-financed by the European Union from the European Regional Development Fund. The content of the site is the sole responsibility of Serengeti ltd.
cross