From the course: Kotlin Multiplatform Development
KOIN for dependency injection - Kotlin Tutorial
From the course: Kotlin Multiplatform Development
KOIN for dependency injection
- [Instructor] Dependency injection is very popular among experienced software developers. It offers improvements to the decoupling, testability, and reusability of software components. For Kotlin Multiplatform, the most popular dependency injection solution of choice is called Koin, with a K. It has a simple, easy to use DSL, domain specific language, and can be used with annotations to simplify adding it to your classes. Let's start by adding Koin to our project. Then we should add dependency injection to improve our existing code. As usual, we should add the actual library dependencies to the project. If we go to gradle folder, libs.versions.toml, we add Koin version 4.0.4 is the current one right now, maybe newer when you look at it. And then we add three actual libraries, koin-android, specific to Android, koin-compose-viewmodel, for injecting view models, and koin-core. We don't end up needing a plugin for this. So when we go over here, again, we don't need a plugin. We add koin-android under androidMain.dependencies. We add koin-core and koin-composed-viewmodel under the commonMain.dependencies, and that's it there, and there's nothing else to do in that file. Next, we need to add the code to initialize Koin on startup. We need to run the startKoin command for each platform with all the modules and the logging configured. When producing multi-platform projects, there's a common Koin pattern where the project will have a list of modules including a common module and a platform module. By defining the platform module in shared code as expect val platformModule, as we do here, we can define it differently for each platform. First, let's identify common code modules to add to the common module. We can add the MainViewModel with viewModelOf, the HTTP client, the NewsApi, the SQL driver, and our database. Every one of these can be added as a singleton, except for the view model, using the singleOf method. The view model has a special viewModelOf method to use instead, because it has to be injected differently. Now that we have our common modules to find in a module, to identify any platform specific modules to add, at this point, there are none. Every library that we've used in this project has shared code provided for each platform, but it's good practice to have these modules ready. So we're going to create an empty platform module for each platform. Here's desktopMain, iOS, and Android. At this point, we should wire up the Koin initialization. Android needs special configuration due to Android's context, so we'll need to create a custom Android application class and wire up its onCreate in order to start Koin. We'll first create an AndroidApp class extending from Android's application class as we do here. Then after overriding the onCreate here as I do, we also have to add this name parameter to our application. This ensures that our app is started up using this new custom application. Here, we run startKoin. We pass in the androidLogger class. We pass androidContext with the application context. And we pass modules of appModule, which as you remember, is a list of the common module with everything in it and the platform module with everything in it. And like I said, we have to make sure that we define our app here. Otherwise, none of this code is going to get used when we start our app. Now we have to do the same kind of thing for iOS. However, we have a initKoin class in shared code here. This can be reused for both iOS and desktop, basically for all platforms that don't need anything special as far as context or the Android logger. Here, we just set the printLogger for everything debug and above, and we pass the appModule again. Now, if we go over to iOSApp.swift, I'm going to wire it this way, I'm going to add a UIApplicationDelegateAdaptor, and create an AppDelegate, so that I can then import ComposeApp. ComposeApp here is our XCFramework, our shared library that I created, I'll show you. Right here, we created ComposeApp as the baseName, and we made it static. You can do this a lot of ways, but here we have all of our iOS logic for shared code bound up into a shared framework called ComposeApp. So here we import ComposeApp, and then that makes available the KoinSharedKt.doInitKoin method. This looks really ugly, but this is a builtin method of the UI application delegate. So we are just adding a new function call to something builtin that needs to be overridden. Anytime our application starts up, we initialize Koin. Now we need to wire up each of the places where we should be injecting our classes instead of constructing them on the fly. Remember in NewsApi, if go there, we were originally creating our client on the fly, but now we actually create our client here with the client KT method that does this. So we can say singleOf client, and it'll reference this, and we no longer need to do it on the fly here. We can just say, by inject. And this is important, NewsApi needs to subclass KoinComponent. This makes this inject method function. In MainViewModel, we should be injecting our NewsApi by inject as well. This makes sure it's the same instance, because we use singleOf, remember? And the database, we were using this createDatabase call. Now we can just say by inject. As you can see here, we call createDatabase like this. We no longer need to have all the extra junk. We can simply have createDriver and createDatabase as singles here, and all of the magic happens in MainViewModel. It just finds it by doing CacheDB by inject. Again, I add KoinComponent here to our MainViewModel so that we can inject to it. Finally, we need to do one more thing. To make everything work in compose, and to be able to inject into compose, get rid of that, we add a KoinContext surrounding the top level of the entire compose app. This then allows me to inject things with things like KoinViewModel or the call to get or call to inject. Remember, I used to have to define my view model here in the constructor with a default value. Now I can just inject it, and it's going to always use the right view model. This right here, and that's all coming again from this. At this point, the app should now run on all platforms and we're now properly using dependency injection to share singletons instead of passing in a brand new instance each time that could cause us terrible bugs down the road. This should make it more testable and much easier to use.
Practice while you learn with exercise files
Download the files the instructor uses to teach the course. Follow along and learn by watching, listening and practicing.