Going Modular – The Kotlin Multiplatform Way

Intro

I’m using TMDB to fetch Tv Show information. Here’s how it looks on both platforms. I have a lot of work done on Android since that’s my territory. I’m updating the iOS side of things as I learn. You can find the source code for the project on Github.

TvManiac

I’m using TMDB to fetch Tv Show information. Here’s how it looks on both platforms. I have a lot of work done on Android since that’s my territory. I’m updating the iOS side of things as I learn. You can find the source code for the project on Github.

The Shared Module

Before talking about modularisation, let me share why I decided to do this. The shared module contains common logic for both platforms. (iOS & Android). To be precise, we are sharing the Network and Cache code. There are a couple of great introduction articles at the end of the post. Below is the shared module initially looked at.

So why modularise the shared module? Well, I was initially using packages to structure code. It worked but it was a pain navigating through packages. Since I plan on updating this project, why not do things the right way. And that folks, is why we are here.

Going Modular

This is what the shared module looks like after a couple of experiments here and there. Let’s take a look at the project structure.

Project Structure:

We have a couple of modules but we can group them into four main directories/modules.

I left out the interactor module. I’m calling this the Limbo module for now. This contains interactor classes that don’t have domain logic or are not necessarily features. This will change and move into the domain module as we continue adding features.

Kmm precompiled script

Since most of the module’s build.gradle files are the same; we can use a precompiled script to save a lot of repetition. We create a file kmm-domain-plugin.gradle.kts in the buildSrc directory.


@file:Suppress("UnstableApiUsage")
import Kmm_domain_plugin_gradle.Utils.getIosTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import util.libs
plugins {
kotlin("multiplatform")
id("com.android.library")
}
kotlin {
android()
val iosTarget = getIosTarget()
iosTarget("ios") {}
sourceSets.all {
languageSettings.apply {
optIn("kotlin.RequiresOptIn")
}
}
}
android {
compileSdk = libs.versions.android.compile.get().toInt()
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig {
minSdk = libs.versions.android.min.get().toInt()
targetSdk = libs.versions.android.target.get().toInt()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
object Utils {
fun KotlinMultiplatformExtension.getIosTarget(): (String, KotlinNativeTarget.() -> Unit) -> KotlinNativeTarget =
when {
System.getenv("SDK_NAME")?.startsWith("iphoneos") == true -> ::iosArm64
System.getenv("NATIVE_ARCH")?.startsWith("arm") == true -> ::iosSimulatorArm64
else -> ::iosX64
}
}


We can then add the plugin to the module and get rid of a lot of code. Now, we need to apply the script and add the module dependencies. As you can see, one advantage of this is having lean modules and don’t need to have unnecessary dependencies.


plugins {
`kmm-domain-plugin`
}
dependencies {
commonMainImplementation(project(":shared:core"))
commonMainImplementation(project(":shared:database"))
commonMainImplementation(project(":shared:domain:seasons:api"))
commonMainImplementation(libs.kotlin.coroutines.core)
}


Exposing dependencies on iOS

Now that we have our modules, we need to add them to the shared module since this is the entry point for iOS. By adding the modules to commonMain, Kmm will generate an Obj-C Framework.

To expose the dependencies, we do two things


   sourceSets["commonMain"].dependencies {
api(project(":shared:core"))
api(project(":shared:database"))
api(project(":shared:remote"))
}

2. Export the modules in the Framework configuration.


    targets.withType<KotlinNativeTarget> {
binaries.withType<Framework> {
isStatic = false
linkerOpts.add("-lsqlite3")
export(project(":shared:core"))
export(project(":shared:database"))
export(project(":shared:remote"))
export(project(":shared:domain:show:api"))
export(project(":shared:domain:seasons:api"))
export(project(":shared:domain:episodes:api"))
export(project(":shared:domain:genre:api"))
transitiveExport = true
}
}

You’ll notice we are only adding api modules to the framework configuration. This is because we only need to expose the interfaces. 😎

One last thing, and we can wrap things up. Let’s look at Dependency injection.

Dependency injection

I’m using what I want to call a mixed injection where Android uses Dagger Hilt, and Shared module uses Koin to initialize the graph. I’m using Koin in shared so we can provide dependencies for iOS. So we’ll just look at how we are setting it up in the shared module.

Most modules have a di package that contains module dependencies. As an example, this is what genre looks like so:


val genreModule: Module = module {
single<GenreRepository> { GenreRepositoryImpl(get(), get(), get()) }
single<GenreCache> { GenreCacheImpl(get()) }
factory { GetGenresInteractor(get()) }
}

We can now add them to the main dependency graph in the shared module koin class.


fun initKoin(appDeclaration: KoinAppDeclaration = {}) = startKoin {
appDeclaration()
modules(
serviceModule,
cacheModule,
dispatcherModule,
genreModule,
corePlatformModule(),
dbPlatformModule(),
remotePlatformModule()
)
}

We need to add a class in the iosMain directory. It’s an empty function that calls the main init function. Doing this will allow us to initialize the graph.


fun initKoinIos(): KoinApplication = initKoin {}

I might not have mentioned that I’m creating a swift package and adding that to the iOS app. John O’Reilly has a fantastic article on how to go about it. Read more here. (Follow him if you want to get lost in the KMM universe. He does cover a lot of topics and shares valuable resources.)

Summary

I hope this was useful as I walked you through how I approached this challenge. I know I didn’t go deep into things. Mostly because I’m sure there are other ways of doing this. Probably better/more straightforward ways. If you have some insights, please feel free to reach out. Learning is an endless loop, and I’m up for it.

What I love about Kotlin Multiplatform is:

Until we meet again. Adios.✌️

Resources

(Visited 96 times, 1 visits today)

Leave a comment

Your email address will not be published. Required fields are marked *