In a KMM project, the goal is to maximize code reuse. The business logic remains consistent across platforms, so it’s logical to have the domain layer, written in pure Kotlin, in the shared module. This layer can be reused across web, mobile, and terminal platforms. Here, we define the contracts or interfaces for repositories and interactors. In our example, we have a single interactor called GetMoviesInteractor
, responsible for retrieving movies. It depends on the MovieRepository
interface in the domain
package.
class GetMoviesInteractorImpl(private val repo: MovieRepository) {
override suspend fun execute(): List {
return repo.getMovies()
}
}
In the domain layer, the classes and functions are independent of any specific platform framework, such as iOS or Android. They rely only on their own definitions. The GetMoviesInteractor
constructor expects an object that implements the MovieRepository
interface and returns objects within the same layer.
You may question the need for a use case that simply calls a repository function. While it may not appear significant initially (especially in small apps), using interactors can simplify the construction and maintenance of business logic.
For instance, let’s say our Movie
model has a directorDetail
attribute of type MovieDirector
. The movie repository might not have the director information, requiring us to consult another repository for the details.
class GetMoviesInteractorImpl(
private val moviesRepo: MovieRepository,
private val directorsRepo: MovieDirectorRepository,
private val productionCompanyRepository: ProductionCompanyRepository
) {
override suspend fun execute(): List {
val movies = moviesRepo.getAll()
val moviesDetail = movies.map { movie ->
MovieDetail(
movie = movie,
director = directorsRepo.getDirectorDetailById(movie.directorId),
productionCompany = productionCompanyRepository.getCompanyDetailById(movie.productionCompanyId)
)
}
return moviesDetail
}
}
In the shared
module, we organize our shared application core as follows:
androidMain
: This includes a SqlDelight implementation for Android, serving as the database driver factory.iosMain
: Similar toandroidMain
, this implements the SqlDelight database instance for iOS.commonMain
: This is where the multiplatform magic happens. It contains divisions for thedomain
anddata
layers. Thedomain
package includes the interactor,Movie
model, and repository interface. Additionally, we have aDispatcherProvider.kt
defining the Dispatchers for Kotlin Coroutines, to be used in data layer implementations of repositories or data sources.
In the androidMain
and iosMain
packages, you’ll find platform-specific Kotlin code, such as the SQLight database manager. However, the business/domain layer code is located in commonMain
for maximum reusability and independence.
It’s important to note that the domain layer, implemented in the domain
package within the shared/commonMain
module, will be reused by both platforms and the data layer. Hence, its implementation is not included in androidMain
and iosMain
.
One more thing: make sure to add the Kotlin Coroutines dependency to our shared
module to leverage its power.
// shared/build.gradle.kts
sourceSets {
val coroutineVersion = "1.6.0-native-mt"
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion") {
version { strictly(coroutineVersion) }
}
}
}
...
}
Stay tuned for the next post, where we’ll explore the structure of the data layer!
You can follow the progress of the project on GitHub:
Github - jflavio11/LayeredKotlinMultiplatform
Example of Mobile Kotlin Multiplatform app for showing about layered architecture
github.com
What’s Next
This has been the third post in which we have talked about the domain layer. In the following, we will talk about the data layer. This guide is divided into the following posts:
- Introduction
- Designing the solution
- Creating the domain layer (this post)
- Creating the layer data
- Implementing the presentation layer