Intro
TvManiac
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.
- core: Contains all “common” classes used by all modules. These can be util classes like CoroutineScope/Dispatchers to abstract classes.
- remote: Module with Ktor implementation
- database: DB implementation. We are using SQLDelight for this project.
- domain (Feature): Domain modules are more like feature modules. They ideally match the features in the app. They have two modules in them:
- api: They contain interfaces. We also export these modules so iOS has access.
- implementation: As the name suggests, this module contains implementation details of the api module.
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
- Add the modules as api in
commonMain
source set
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 {
binaries.withType {
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 { GenreRepositoryImpl(get(), get(), get()) }
single { 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:
- Most of the core business logic is shared. This allows me to spare some time and take a dive into the Apple world.
- UI development is Native. So I can still keep up on what’s happening on Android and learn a bit of Swift while at it.
Until we meet again. Adios.✌️