Welcome to the fourth part of our series, where we dive into the data layer of our KMM application. If you missed our previous articles on architecture design, be sure to catch up.
We have three main modules: android
, iOS
, and core
(named shared
).
In the shared
module, under xMain package, we cater to various platforms like web, terminal, and Mac. Let’s focus on the key elements in this part:
commonMain/data
: This is where we implement the data layer, handling data access, repository interfaces, and data sources. Platform-specific database engines are excluded.androidMain
: Here, we tackle android-specific code that is reusable across the project’s android app modules. We implement the expect classes from thecommonMain
module.iosMain
: Similar to androidMain, but for iOS-specific code implementation.
Important note
In the shared module, the androidMain
and iosMain
modules do not directly implement the data access layer, but rather they implement expect classes. These expect classes define contracts for platform-specific functionality.
For instance, obtaining the current timestamp may vary between Android and iOS. By creating an expect class, we establish a contract for getting the timestamp, which each platform module then implements accordingly. This approach ensures consistent behavior while accommodating platform differences.
// inside the shared/commonMain module
expect class TimeProvider {
var timestamp: Long
}
// implementation in shared/androidMain module
actual class TimeProvider {
actual var timestamp: Long = System.currentTimeMillis()
}
// implementation in shared/iosMain module
actual class TimeProvider {
actual var timestamp = NSDate().timeIntervalSince1970.toLong()
}
The same applies for the way how each platform can create an instance of the database engine. We only define the “contract” for getting the database driver:
expect class DatabaseDriverFactory {
fun createDriver(): SqlDriver
}
And each platform implements the expect class, for example, Android requires a context
:
// implementation inside shared/androidMain module
actual class DatabaseDriverFactory(private val context: Context) {
actual fun createDriver(): SqlDriver {
return AndroidSqliteDriver(
schema = MoviesDb.Schema,
context = context,
name = "movies.db"
)
}
}
This is how the data layer is looking in our project (as you can see, the data package is in the shared/commonMain
module):
We have there the domain package for the domain layer as I explained in the previous post, and sqldelight
package outside (just below the Kotlin package), and the data package where all the data layer is implemented.
Inside our data layer, we have:
local
package: local data access, here we can define an expect class for platform data access implementation (likeSharedPreferences
in Android), or, as our project, the expect class for creating the database driver (DatabaseDriverFactory.kt
).
TheMovieLocalDataSource
is responsible for the CRUD operations related to a local data source (internal database, system preferences, etc). It’s going to be used in the repository implementation (MovieRepositoryImpl
).remote
package: as the name suggests, here we have all code related to remote data access, that is: REST APIs, GraphQL, Firebase, etc. We are only using a REST API from the MovieDB service. Moreover, we can define the responses and requests, these must be the same for any platform, that’s why is defined here.TimeProvider
andPropertiesProvider
: expect classes that returns platform specific data,TimeProvider
returns the current timestamp andPropertiesProvider
the API KEY for consuming the REST API.MovieRepositoryImpl
: the implementation of the MovieRepository interface inside the domain layer. It’s very easy to notice that this is shared by any platform: we request the movies list, we ask inside our local data source is the data is outdated, if it is, then we request to the REST API, we save the result in the local data source (that is, we update it) and return the updated data.
This is going to be invoked by theGetMoviesInteractor
inside our domain layer, and theGetMoviesInteractor
should be injected into aViewModel
,Presenter
, etc.
Note: I recommend to you to read the SQDelight library documentation to understand how it works, it’s easy to implement but there are some details to know such as setting the same package structure for defining the database scheme.
In Summary
In this Kotlin Multiplatform project:
The data layer is implemented in the
commonMain
module of theshared
module for maximum reusability.Classes in the data layer are injected into domain layer classes, but the injection occurs on each platform.
If platform-specific data is required, expect classes are defined and implemented in the platform modules within the
shared
module.It is advisable to keep the data layer logic within the
data
package, as repository functionality and CRUD operations may vary by platform.
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 fourth 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
- Creating the layer data (this post)
- Implementing the presentation layer