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 the commonMain 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):
The data layer in the 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 (like SharedPreferences in Android), or, as our project, the expect class for creating the database driver (DatabaseDriverFactory.kt).
    The MovieLocalDataSource 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 and PropertiesProvider: expect classes that returns platform specific data, TimeProvider returns the current timestamp and PropertiesProvider 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 the GetMoviesInteractor inside our domain layer, and the GetMoviesInteractor should be injected into a ViewModel, Presenter, etc.

The MovieRepository implementation inside the data layer.

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 the shared 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:

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:

  1. Introduction
  2. Designing the solution
  3. Creating the domain layer
  4. Creating the layer data (this post)
  5. Implementing the presentation layer