This is the second article in an article series that will discuss the dependency injection and service locator patterns, explain how they differ, and detail the most prominent Multiplatform Mobile libraries and approaches to implementing these patterns. This article introduces the first library: Koin. You can find the first article in the series here.
Koin is a well-known open-source Service Locator dependency management library that has support for Android, Ktor, and Multiplatform.
A Little Boilerplate
We start off with a little bit of boilerplate code running across all our articles. Recall that our example is that of a search box on a company intranet enabling us to search across staff members.
data class StaffMember(val name: String, val position: String)
interface StaffLister {
fun findAllStaffMembers(): List<StaffMember>
}
class StaffListerImpl : StaffLister {
override fun findAllStaffMembers(): List<StaffMember> {
return listOf(
StaffMember(
"Pamela Hill",
"Developer Advocate"
)
)
}
}
class SearchBox(private val lister: StaffLister){
fun findStaffMemberByName(name: String): StaffMember? {
return lister.findAllStaffMembers().firstOrNull {
name == it.name
}
}
}
Initialization
To initialize Koin, it is necessary to call the startKoin function with one or more modules of dependency bindings within the context of the application. The code below contains the definition of the application module and an initKoin function. The initKoin function then needs to be called during the right parts of the application lifecycle initialization.
commonMain/…/SearchBox.kt
import org.koin.core.KoinApplication
import org.koin.core.context.startKoin
import org.koin.core.module.Module
import org.koin.dsl.module
...
object Modules {
val appModule = module {
factory { StaffListerImpl() as StaffLister }
factory { SearchBox (get()) }
}
}
fun initKoin(
appModule: Module = Modules.appModule
): KoinApplication = startKoin {
modules(appModule)
}
Android
androidApp/…/MyApplication.kt
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
initKoin()
}
}
iOS
Note that we are using a bridge from the iOS application to the iOS shared code to the initKoin function, which is unnecessary in the Android application.
iosMain/.../Helper.kt
fun initKoinApp(){
initKoin()
}
iosApp/…/iOSApp.swift
@main
struct iOSApp: App {
init() {
HelperKt.doInitKoinApp()
}
…
}
Usage
In order to hook the user interface up to the search box implementation, it is necessary to inject the SearchBox into a user interface component. This is fairly straightforward for Android, but on iOS, we once again have to use a bridge from the iOS application to the iOS shared code, where the SearchBox is injected (notice also that the “bridge” is a KoinComponent).
Android
androidApp/…/MainActivity.kt
import org.koin.android.ext.android.inject
...
class MainActivity : AppCompatActivity() {
private val searchBox: SearchBox by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val tv: TextView = findViewById(R.id.text_view)
tv.text = searchBox
.findStaffMemberByName("Pamela Hill").toString()
}
}
iOS
iosMain/…/SearchBoxHelper.kt
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
...
class SearchBoxHelper : KoinComponent {
private val searchBox : SearchBox by inject()
fun findByName(name: String) : String =
searchBox.findStaffMemberByName(name).toString()
}
iosApp/…/ContentView.swift
struct ContentView: View {
let staff = SearchBoxHelper().findByName(name: "Pamela Hill")
var body: some View {
Text(staff)
}
}
Opinion
Koin is a lovely dependency management library for multiplatform development. I have developed a large-scale Android app with it, after finding that Dagger was too complicated for the team to understand (this was before Hilt). The team behind it is passionate and takes even harsh criticism from the community in their stride, taking it as constructive and using it to make the library even better. I have two examples of this:
Criticism 1: Since there is no compile-time check as with Dagger, it can lead to crashes when a needed dependency is omitted
While there is no way to make Koin a code-generating DI library, they took this criticism and introduced a way to check your module dependency graph during your unit testing cycle. This clever move allows you to make checking your modules part of your CI/CD pipelines and your app won't hit production with such a blaring unit test failure, assuming you remember to add all your modules.
class CheckModulesTest : KoinTest {
@Test
fun verifyKoinApp() {
koinApplication {
modules(module1, module2)
checkModules()
}
}
}
Criticism 2: All the calls to get() for long dependency lists quickly gets out of hand
In the versions of Koin before 3.2, long dependency lists meant long argument lists of get() arguments being passed mindlessly to constructors. But in version 3.2, a new constructor DSL was introduced that eliminated this problem. For example:
module {
factory { StaffListerImpl() as StaffLister }
factory { SearchBox (get()) }
}
became instead:
module {
factoryOf(::StaffListerImpl) bind StaffLister::class
factoryOf(::SearchBox)
}
Now, imagine the benefit if SearchBox had several dependencies!
The remainder of this series will focus on the actual libraries and approaches for dependency management in Multiplatform Mobile, and will feature articles on:
Kodein
Kotlin-Inject
Manual injection
Stay tuned!
References
Copyright © 2024 JetBrains s.r.o.
Commentaires