KMPizza
Kotlin Multiplatform + Pizza = ❤️

Step 14: Glue everything together with Koin dependency injection

This time we’re going to look into dependency injection with Koin.
In a simple app you can get away with manually injecting every dependency you need.
However, the bigger your app becomes, the more reasonable it is to use a dedicated dependency injection framework.

First, add core dependency to the Versions.kt:

 
object Common { 
. . .
const val KOIN_CORE = "io.insert-koin:koin-core:$KOIN_VERSION"
}

And implement it in the shared build.gradle.kts in commonMain with:

 
val commonMain by getting {
   dependencies {
       . . .
       api(Versions.Common.KOIN_CORE)
   }
}

Sync your project files.

Now instead of using RecipeRemoteSource directly to fetch data let’s create a repository for our recipes. In the future, it will manage data between our remote source, which we’ve already implemented and the local source, which we’ll implement later with SQLDelight.

Add a repository package to your commonMain.
Create a RecipeRepository.kt there.

create repository

Now that we have Koin, we can create a recipe repository by making it a KoinComponent and injecting a recipeRemoteSource:

 
class RecipeRepository : KoinComponent {
   private val recipeRemoteSource: RecipeRemoteSource by inject()

   suspend fun postRecipe(recipe: Recipe): Long = recipeRemoteSource.postRecipe(recipe)
   suspend fun getRecipes(): List<RecipeResponse> = recipeRemoteSource.getRecipes()
}

This is a very simple repository, because we don’t have any other source but the backend now.

Likewise, you can replace RecipeRemoteSource with RecipeRepository in your RecipeViewModel by simply injecting it:

 
class RecipeViewModel : CoroutineViewModel(), KoinComponent {
   private val recipeRepository: RecipeRepository by inject()
. . .
}

Don’t forget to change getRecipes():

 
fun getRecipes() {
   coroutineScope.launch {
       _recipes.value = recipeRepository.getRecipes()
   }
}

Now you can remove all the manual injection of RecipeRemoteSource in MainActivity, RecipesScreen and MainScreen.

Use RecipeViewModel in RecipesScreen just like this:

 
public fun RecipesScreen() {
   val viewModel = remember {
       RecipeViewModel()
   }
   val recipes by viewModel.recipes.collectAsState()

   Recipes(items = recipes)
}

Your MainActivity will look much prettier now with no manual injection:

 
class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           MaterialTheme {
               MainScreen()
           }
       }
   }
}

Try running the app now and you’ll get an error message:
java.lang.IllegalStateException: KoinApplication has not been started

That is because we have not applied our universal Koin glue yet!
We used dependency injection everywhere, but our app doesn’t know about it.
Therefore we first need to glue the shared and android platform code together.

First create a di folder in the shared commonMain and add a CommonModule.kt there with the following code:

 
fun initKoin(appDeclaration: KoinAppDeclaration) [1] = startKoin {
   modules( [2]
       apiModule
   )
}

private val apiModule = module {
   single<KtorApi> { KtorApiImpl() } [3]
   factory { RecipesApi(get()) }
}

[1] This will initialize Koin on the platform side. We’ll see how to use it later, when writing the platform-specific code.
[2] Here we’ll keep all the modules that we need for dependency injection. For now it’s just our networking.
[3] We create a singleton of KtorApi and then use a factory to create RecipesApi whenever we need one.

Similarly, we’ll add a viewModel module and a repositoryModule.
The final version has to look like this:

 
fun initKoin(appDeclaration: KoinAppDeclaration) = startKoin {
   appDeclaration()
   modules(
       apiModule,
       repositoryModule,
       viewModelModule
   )
}


private val apiModule = module {
   single<KtorApi> { KtorApiImpl() }
   factory { RecipesApi(get()) }
}

private val viewModelModule = module {
   single{ RecipeViewModel() }
}

private val repositoryModule = module {
   factory { RecipeRemoteSource(get()) }
   single { RecipeRepository() }
}

Now we’re only missing platform-related Koin dependencies.
Add this to your Versions.kt:

 
object Android {
const val KOIN_ANDROID_MAIN = "io.insert-koin:koin-android:$KOIN_VERSION"
. . .
}

In Android app build.gradle:

 
implementation(Versions.Android.KOIN_ANDROID_MAIN)

In the androidApp create a MainApp class which will extend the Application class create MainApp

Start the Koin application in MainApp.kt:

 
@Suppress("unused")
class MainApp : Application() {

   override fun onCreate() {
       super.onCreate()
       initKoin { 
       }
   }
}

Finally, we need to inform the android app about our starting point, which we do in the Manifest by adding this line to application:

 
   android:name=".MainApp"

Run the app again and you’ll see “Pizza dough” on the main screen.
With the help of Koin our project looks neater and it’s still working just like before!

Now that our android app is up and running, in the next step we’ll do the same for the iOS app by binding iOS UI to the shared KMM ViewModel.