Chapters

Hide chapters

Kotlin Multiplatform by Tutorials

Second Edition · Android 14, iOS 17, Desktop · Kotlin 1.9.10 · Android Studio Hedgehog

9. Dependency Injection
Written by Saeed Taheri

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Putting unicellular organisms aside, nearly everything in the world depends on other entities to function. Whether it’s something in nature or something mankind has created, it usually takes multiple things to create a working instance of anything.

Imagine an assembly line in a car factory. They don’t create the engines and the wheels on the assembly line. Car manufacturers outsource many of the parts to other companies. In the end, they bring them all to the assembly line, inject each part into the making-in-progress and a shiny new car appears. The car is dependent on other objects. The same applies to the software world.

If you were to model the Car into a class, one of its dependencies would be the Engine. The car object shouldn’t be responsible for creating the engine. You should inject the engine from outside into the assembly line — or in programming nomenclature, constructor, or initializer.

Advantages of Dependency Injection

Dependency injection, or DI, has many advantages.

  • Maintainability: DI makes your code maintainable. If your classes are loosely coupled, you can catch bugs more easily and address a possible issue faster than you would with a convoluted class that doesn’t adhere to the single-responsibility principle.
  • Reusability: Going back to the car factory example, you’re able to reuse the same model of wheels for many cars the factory manufactures. Loosely coupled code will let you reuse many parts of your code in different ways.
  • Ease of refactoring: There may come a time in the lifetime of your app when you need to apply a change to your codebase. The less coupled your classes are, the easier the process will be. Imagine you needed to change the engine if you wanted to have new headlights!
  • Testability: Everything comes back to the code being loosely coupled. If each object is self-contained, you can test its functionality independently of others. No one would like a car whose engine wouldn’t work when a windshield wiper is broken! This way, each team responsible for each module will test their product and hand it over to other teams.
  • Ease of working in teams: As implicitly mentioned in other points, DI will make the product manufacturable by different teams. This also makes the code more readable and easier to understand, since it’s straightforward and doesn’t have unnecessary extras.

Automated DI vs. Manual DI

Now that you’re on the same page with those who favor using dependency injection in their apps, you need to actually provide the dependencies where needed.

class RemindersViewModel(
  private val repository: RemindersRepository
) : BaseViewModel() { // ...
}
Fig. 9.1 — No value passed for repository
Wel. 9.6 — Ke nudaa babvec wac yejejolehv

Setting Up Koin

Setting up Koin is similar to how you’ve set up other multiplatform libraries in the previous chapters – a shared part and some specific libraries to use for each platform.

koin = "3.4.3"
koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" }
koin-test = { group = "io.insert-koin", name = "koin-test", version.ref = "koin" }
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version = "3.4.6" }
implementation(libs.koin.core)
implementation(libs.koin.test)
implementation(libs.koin.android)
implementation(libs.koin.androidx.compose)
implementation(libs.koin.core)

Declaring Your App Dependencies for Koin

Koin uses a special Kotlin Domain Specific Language — or DSL — to let you describe your application and its dependency graph.

package com.yourcompany.organize

object Modules {
  val repositories = module {
    factory { RemindersRepository() }
  }
}
val viewModels = module {
  factory { RemindersViewModel(get()) }
}
fun initKoin(
  appModule: Module = module { },
  repositoriesModule: Module = Modules.repositories,
  viewModelsModule: Module = Modules.viewModels,
): KoinApplication = startKoin {
  modules(
    appModule,
    repositoriesModule,
    viewModelsModule
  )
}
import com.yourcompany.organize.data.RemindersRepository
import com.yourcompany.organize.presentation.RemindersViewModel
import org.koin.core.KoinApplication
import org.koin.core.context.startKoin
import org.koin.core.module.Module
import org.koin.dsl.module

Using Koin on Each Platform

Now that you’ve got all the pieces, it’s time to use Koin for real.

Android

Open OrganizeApp.kt in the androidApp module. Add the onCreate function below inside the class and start Koin there.

override fun onCreate() {
  super.onCreate()

  initKoin(
    viewModelsModule = module {
      viewModel {
        RemindersViewModel(get())
      }
    }
  )
}
@Composable
fun RemindersView(
  viewModel: RemindersViewModel = getViewModel(),
  onAboutButtonClick: () -> Unit,
) {
  // ...
}
Fig. 9.2 — The Android app after integrating Koin.
Fuc. 1.9 — Vba Ohgfeax ecf upsik iswamnosifk Koeg.

iOS

Koin is a Kotlin library. Lots of bridging occurs, should you want to use it with Swift and Objective-C files and classes. To make things easier, you’d better create some helper classes and functions.

package com.yourcompany.organize

object KoinIOS {
  fun initialize(): KoinApplication = initKoin()
}
@kotlinx.cinterop.BetaInteropApi
fun Koin.get(objCClass: ObjCClass): Any {
  val kClazz = getOriginalKotlinClass(objCClass)!!
  return get(kClazz, null, null)
}
@kotlinx.cinterop.BetaInteropApi
fun Koin.get(objCClass: ObjCClass, qualifier: Qualifier?, parameter: Any): Any {
  val kClazz = getOriginalKotlinClass(objCClass)!!
  return get(kClazz, qualifier) { parametersOf(parameter) }
}
import Shared

final class Koin {
  //1
  private var core: Koin_coreKoin?

  //2
  static let instance = Koin()

  //3
  static func start() {
    if instance.core == nil {
      let app = KoinIOS.shared.initialize()
      instance.core = app.koin
    }
    if instance.core == nil {
      fatalError("Can't initialize Koin.")
    }
  }

  //4
  private init() {
  }

  //5
  func get<T: AnyObject>() -> T {
    guard let core else {
      fatalError("You should call `start()` before using \(#function)")
    }

    guard let result = core.get(objCClass: T.self) as? T else {
      fatalError("Koin can't provide an instance of type: \(T.self)")
    }

    return result
  }
}
@main
struct iOSApp: App {
  init() {
    Koin.start()
  }
  // ...
}
let viewModel: RemindersViewModel = Koin.instance.get()
Fig. 9.3 — The iOS app after integrating Koin.
Ceb. 8.4 — Vwu oIK icw emdur exdazhikecv Moat.

Desktop

This is the easiest of all platforms. First, open Main.kt and add a reference to the Koin object. Initialize it in the main function.

lateinit var koin: Koin
  private set

fun main() {
  koin = initKoin().koin

  return application { // ... }
//
@Composable
fun RemindersView(
  viewModel: RemindersViewModel = koin.get(),
  onAboutButtonClick: () -> Unit,
) {
  // ...
}
Fig. 9.4 — The Desktop app after integrating Koin.
Qug. 1.3 — Zne Zawshis olj udfos ubbasbiyifx Naot.

Updating AboutViewModel

You’re now familiar with the process, it’s time to update AboutViewModel to use DI. Put the book down and see if you can do it all by yourself.

class AboutViewModel(
  platform: Platform
) : BaseViewModel() {
  // ...
}
val viewModels = module {
  factory { RemindersViewModel(get()) }
  factory { AboutViewModel(get()) }
}
object Modules {
  val core = module {
    factory { Platform() }
  }
  // ...
}
fun initKoin(
  appModule: Module = module { },
  coreModule: Module = Modules.core,
  repositoriesModule: Module = Modules.repositories,
  viewModelsModule: Module = Modules.viewModels,
): KoinApplication = startKoin {
  modules(
    appModule,
    coreModule,
    repositoriesModule,
    viewModelsModule,
  )
}

Android

Open AboutView.kt in the androidApp module and change the AboutView composable definition to this:

fun AboutView(
  viewModel: AboutViewModel = getViewModel(),
  onUpButtonClick: () -> Unit
) {
  // ...
}
initKoin(
  viewModelsModule = module {
    viewModel {
      RemindersViewModel(get())
    }
    viewModel {
      AboutViewModel(get())
    }
  }
)

iOS

It’s pretty straightforward. Open AboutView.swift and change the definition of viewModel as follows:

@State private var viewModel: AboutViewModel = Koin.instance.get()

Desktop

This is also a piece of cake. Open AboutView.kt in desktopApp module and change the AboutView composable function definition:

fun AboutView(
  viewModel: AboutViewModel = koin.get()
) {
  ContentView(items = viewModel.items)
}

Testing

Because of the changes you made in this chapter, the tests you wrote in the previous chapter wouldn’t compile anymore. Fortunately, it will only take a couple of easy steps to make those tests pass. You’ll also learn a few more tricks for testing your code along the way.

Checking Koin Integration

As you already know, Koin resolves the dependency graph at runtime. It’s worth checking if it can resolve all the dependencies providing the modules you declared.

package com.yourcompany.organize

class DITest {
  @Test
  fun testAllModules() {
    koinApplication {
      modules(
        Modules.viewModels,
      )
    }.checkModules()
  }
}
Fig. 9.5 — Koin checkModules test failed
Qax. 7.2 — Zueb wzidmGocegos gahr qousul

modules(
  Modules.core,
  Modules.repositories,
  Modules.viewModels,
)
@AfterTest
fun tearDown() {
  stopKoin()
}

Updating RemindersViewModelTest

Open RemindersViewModelTest.kt. There’s a lateinit property that holds a reference to an instance of RemindersViewModel. In the setup method, you’re initializing this property like this:

viewModel = RemindersViewModel(RemindersRepository())
class RemindersViewModelTest: KoinTest {
  private val viewModel: RemindersViewModel by inject()

  //...
}
@BeforeTest
fun setup() {
  initKoin()
}

@AfterTest
fun tearDown() {
  stopKoin()
}

Key Points

  • Classes should respect the single-responsibility principle and shouldn’t create their own dependencies.
  • Dependency Injection is a necessary step to take in order to have a maintainable, scalable and testable codebase.
  • You can inject dependencies into classes manually, or use a library to do all the boilerplate codes for you.
  • Koin is a popular and declarative library for DI, and it supports Kotlin Multiplatform.
  • You declare the dependencies as modules, and Koin resolves them at runtime when needed.
  • When testing your code, you can take advantage of Koin to do the injections.
  • You can test if Koin can resolve the dependencies at runtime using the checkModules method.
  • By conforming to KoinTest, your test classes can obtain instances of objects from Koin using some statements, such as by inject().

Where to Go From Here?

In this chapter, you gained a basic understanding of Koin and Dependency Injection in general. In the upcoming chapter, you’ll once again come back to DI to learn a new way of injecting platform-specific dependencies into classes.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now