Jetpack Preferences Datastore in Kotlin Multiplatform Mobile (KMM)

Karim Reda
arconsis
Published in
6 min readJul 20, 2023

Kotlin Multiplatform Mobile (KMM) is gaining popularity as a powerful framework for building cross-platform mobile applications. With KMM, developers can share code between different platforms, such as Android and iOS, and leverage the benefits of the Kotlin language. One important aspect of mobile app development is managing and persisting data, and Jetpack Datastore provides a convenient solution for this purpose. In this article, we will explore how to use Jetpack Datastore in Kotlin Multiplatform Mobile.

What is Jetpack Datastore?

Jetpack Datastore is a data storage solution provided by Android Jetpack libraries. It offers a modern, efficient, and easy-to-use API for persisting key-value pairs or complex objects. Datastore supports both synchronous and asynchronous operations, making it suitable for a variety of use cases. It also provides type safety and automatic serialization/deserialization, reducing the chances of runtime errors.

Advantages of Jetpack Datastore in KMM

  1. Cross-platform compatibility: Jetpack Datastore is part of the Android Jetpack libraries, but it can be used in KMM projects to share data storage code between Android and iOS platforms. This allows you to have a unified data storage solution across different platforms, reducing code duplication and ensuring consistent behavior.
  2. Type-safe approach: Modern and efficient API: Jetpack Datastore provides a modern and easy-to-use API for data storage. It offers a type-safe approach, allowing you to store and retrieve data using strongly typed keys and values. This reduces the chances of runtime errors and enhances code readability.
  3. Automatic serialization/deserialization: Datastore automatically handles serialization and deserialization of data, eliminating the need for manual conversion between data objects and storage formats. This simplifies the process of persisting complex data structures and makes it easier to work with custom data types.
  4. Asynchronous operations: Datastore supports both synchronous and asynchronous operations, allowing you to choose the appropriate approach based on your requirements. Asynchronous operations are particularly useful for handling data storage tasks without blocking the main thread, ensuring a smooth user experience.
  5. Performance and efficiency: Jetpack Datastore is designed to be performant and efficient. It employs efficient storage mechanisms and data structures to minimize memory usage and improve read and write operations.
  6. Seamless integration with Kotlin: Jetpack Datastore is built with Kotlin in mind, making it a natural choice for Kotlin-based projects like KMM. It leverages Kotlin’s language features, such as data classes and coroutines, to provide a smooth and idiomatic experience when working with data storage in Kotlin codebases.
  7. Configuration flexibility: Datastore allows you to configure the storage behavior according to your needs. You can define preferences with different modes (e.g., private, shared across processes) and specify the serialization format (e.g., Proto DataStore for custom data structures). This flexibility allows you to adapt the storage behavior to fit your specific requirements.

Jetpack Datastore Types

The Jetpack Datastore offers two primary types of stores, namely Preferences and Proto DataStore. These stores can be directly utilized from the shared module.

  1. Preferences DataStore is a key-value store that allows you to store simple data types, such as strings, integers, booleans, and floats. It is similar to SharedPreferences but provides a more modern and type-safe API.
  2. Proto DataStore is a store that allows you to store custom data types defined using Protocol Buffers. Protocol Buffers provide a flexible and efficient way to define data structures that can be serialized and deserialized automatically by Datastore.

This article focuses solely on the implementation of the Jetpack Preferences Datastore.

Implementing Jetpack Preferences Datastore into a KMM Project

To incorporate Jetpack Preferences Datastore into your KMM project, follow the steps outlined below.

Step 1: Set up your KMM project

Create a new KMM project using the instructions provided by JetBrains. Ensure you have the necessary project structure and configuration for iOS and Android platforms.

If you’re unfamiliar with the process of creating a KMM project, we encourage you to refer to our article on getting started with Kotlin Multiplatform Mobile (KMM). It provides detailed guidance on the topic.

Step 2: Add Jetpack Datastore to your project dependencies

In your KMM project, add the Jetpack Datastore dependency to your shared module’s build.gradle file.

kotlin {
android()
ios()
}
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("androidx.datastore:datastore-preferences-core:<version>")
}
}
}
}

Replace <version> with the latest version of the Datastore library. Once you’ve added the dependency, you can start using Jetpack Datastore in your shared code.

Step 3: Platform-specific configuration

To utilize Jetpack Datastore in each platform (iOS and Android), you are required to instantiate a Datastore object by specifying the respective path.

// in src/commonMain/kotlin
fun createDataStore(
producePath: () -> String,
): DataStore<Preferences> = PreferenceDataStoreFactory.createWithPath(
corruptionHandler = null,
migrations = emptyList(),
produceFile = { producePath().toPath() },
)

internal const val dataStoreFileName = "meetings.preferences_pb"

// in src/androidMain/kotlin
fun dataStore(context: Context): DataStore<Preferences> =
createDataStore(
producePath = { context.filesDir.resolve(dataStoreFileName).absolutePath }
)

// in src/iosMain/kotlin
fun dataStore(): DataStore<Preferences> = createDataStore(
producePath = {
val documentDirectory: NSURL? = NSFileManager.defaultManager.URLForDirectory(
directory = NSDocumentDirectory,
inDomain = NSUserDomainMask,
appropriateForURL = null,
create = false,
error = null,
)
requireNotNull(documentDirectory).path + "/$dataStoreFileName"
}
)

Step 4: Create a Datastore Instance Using Koin

Creating your datastore instance just once and injecting it into your code when necessary is essential for the app’s performance. You can do this using a dependency injection framework like Koin.

// in src/commonMain/kotlin
fun initKoinAndroid(additionalModules: List<Module>) {
startKoin {
modules(additionalModules + getBaseModules())
}
}

fun initKoiniOS(appConfig: AppConfig) {
initKoin(listOf(module { single { appConfig } }))
}

internal fun getBaseModules() = appModule + platformModule

// in src/androidMain/kotlin
actual val platformModule: Module = module {
single { dataStore(get())}
}

// in androidApp/src/main/java/…/app
class App : Application() {
override fun onCreate() {
super.onCreate()
initKoinAndroid(
listOf(
module {
single<Context> { this@App }
}
)
)
}
}

// in src/iosMain/kotlin
actual val platformModule: Module = module {
single { dataStore()}
}

// in iosApp/…/app
@main
struct MeetingsApp: App {
init() {
SetUpKoin.setupNativeCore()
}
}

public enum SetUpKoin {
public static func setupNativeCore() {
KoinKt.doInitKoiniOS(appConfig: config)
}
}

Step 5: Working with Jetpack Datastore Preferences

Within the shared module, you can utilize the Preferences DataStore for the purpose of saving and retrieving data, as demonstrated in the following example.

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

suspend fun updateExampleCounter(newCounter: Int) {
dataStore.edit { preferences ->
preferences[EXAMPLE_COUNTER] = newCounter
}
}

val exampleCounterFlow: Flow<Int?> = dataStore.data
.map { preferences ->
preferences[EXAMPLE_COUNTER]
}

Step 6: Unit Testing for Jetpack Datastore Preferences

Let’s proceed with writing unit tests for our code. Here’s a practical example to demonstrate the process.

Inside the setup() method annotated with @Before, we create the DataStore instance using PreferenceDataStoreFactory.createWithPath(produceFile = {“test_preferences”.toPath()}). We then set the main dispatcher to testDispatcher using Dispatchers.setMain(testDispatcher).

In the tearDown() method annotated with @After, we reset the main dispatcher using Dispatchers.resetMain().

Finally, in the “test DataStore write and read” method annotated with @Test, we use runTest(dispatcher) {}, to execute the test case in a controlled environment, which automatically performs the cleanup, instead of using cleanupTestCoroutines() function.

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import okio.Path.Companion.toPath
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals


@ExperimentalCoroutinesApi
class DataStoreTest {

private val testDispatcher = StandardTestDispatcher()
private lateinit var dataStore: DataStore<Preferences>

@Before
fun setup() {
Dispatchers.setMain(testDispatcher)
dataStore = PreferenceDataStoreFactory.createWithPath(produceFile = { "test_preferences".toPath() })
}

@After
fun tearDown() {
Dispatchers.resetMain()
}

@Test
fun `test DataStore write and read`() = runTest(testDispatcher) {
val key = stringPreferencesKey("test_key ")
val value = "test_value"

dataStore.edit { preferences ->
preferences[key] = value
}

val storedValue = dataStore.data.first()[key]
assertEquals(value, storedValue)
}
}

Conclusion

Jetpack Datastore is a powerful and flexible data storage solution that can be used in Kotlin Multiplatform Mobile (KMM) projects. With its type safety, automatic serialization, and support for both simple preferences and custom data structures, Datastore simplifies the process of managing and persisting data across different platforms. By incorporating Jetpack Datastore into your KMM project, you can efficiently handle data storage requirements and improve the overall user experience of your cross-platform mobile application.

--

--