DEV Community

Rendy Adidarma
Rendy Adidarma

Posted on

How to Implement Preferences DataStore for Compose Multiplatform Mobile (Android and iOS)

What is Preferences Data Store?
Preferences DataStore is a modern way to store small amounts of key-value data in Android, replacing SharedPreferences. It is more efficient because it uses Kotlin Flow for async data handling and ensures data consistency.

By the end of this guide, you’ll have a working DataStore setup that allows you to store and retrieve key-value preferences across both platforms. (in Compose Multiplatform :D)

Photo by [Claudio Schwarz](https://unsplash.com/@purzlbaum?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)

Setting Up Data Store in Compose Multiplatform Project.

In this article, we will use Koin to manage dependency injection in our project. Koin is a lightweight and easy-to-use DI framework that helps us organize and inject dependencies efficiently.
We will set up Preferences DataStore for Android and iOS separately, and use Koin to provide instances of these storage solutions.

Add Dependencies

In your *libs.versions.toml *file add this versions and libraries:

[versions] datastore = "1.1.3" koin = "3.5.6" koinCompose = "1.1.5" koinComposeViewModel = "1.2.0-Beta4" [libraries] datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koinCompose" } koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koinComposeViewModel" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } 
Enter fullscreen mode Exit fullscreen mode

In your shared module/composeApp (build.gradle.kts), add:

sourceSets { androidMain.dependencies { implementation(libs.koin.android) } commonMain.dependencies { implementation(libs.koin.core) implementation(libs.koin.compose) implementation(libs.koin.compose.viewmodel) implementation(libs.datastore) implementation(libs.datastore.preferences) } } 
Enter fullscreen mode Exit fullscreen mode

Create a DataStore instance in **commonMain **package.

Save this as DataStoreInstance.kt in commonMain:

import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.internal.SynchronizedObject import kotlinx.coroutines.internal.synchronized import okio.Path.Companion.toPath @OptIn(InternalCoroutinesApi::class) private val lock = SynchronizedObject() // Used for thread safety private lateinit var dataStore: DataStore<Preferences> // Late-initialized variable @OptIn(InternalCoroutinesApi::class) fun createDataStore(producePath: () -> String): DataStore<Preferences> { return synchronized(lock) { if (::dataStore.isInitialized) { dataStore } else { PreferenceDataStoreFactory.createWithPath(produceFile = { producePath().toPath() }) .also { dataStore = it } } } } internal const val DATA_STORE_FILE_NAME = "storage.preferences_pb" 
Enter fullscreen mode Exit fullscreen mode

Call this function in Android with path argument (DataStoreInstance.android.kt)

import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences fun createDataStore(context: Context): DataStore<Preferences> { return createDataStore { context.filesDir.resolve(DATA_STORE_FILE_NAME).absolutePath } } 
Enter fullscreen mode Exit fullscreen mode

Also, we need to call this function in iOS (DataStoreInstance.ios.kt)

import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import kotlinx.cinterop.ExperimentalForeignApi import platform.Foundation.NSDocumentDirectory import platform.Foundation.NSFileManager import platform.Foundation.NSUserDomainMask @OptIn(ExperimentalForeignApi::class) fun createDataStore(): DataStore<Preferences> { return createDataStore { val directory = NSFileManager.defaultManager.URLForDirectory( directory = NSDocumentDirectory, inDomain = NSUserDomainMask, appropriateForURL = null, create = false, error = null, ) requireNotNull(directory).path + "/$DATA_STORE_FILE_NAME" } } 
Enter fullscreen mode Exit fullscreen mode

DataStore config setup is complete, now we need to setup Koin for Dependency Injection, the purposes is to inject or provide DataStore in our repository *or *viewmodel.

In commonMain, create a DataStoreModule with expected variable inside it:

commonMain (DataStoreModule.kt)

import org.koin.core.module.Module expect val dataStoreModule: Module 
Enter fullscreen mode Exit fullscreen mode

Add the actual declaration in androidMain and iosMain :

Initialize Actual Declaration

androidMain (DataStoreModule.android.kt)

import com.rainday.datastorecmp.createDataStore import org.koin.android.ext.koin.androidContext import org.koin.core.module.Module import org.koin.dsl.module actual val dataStoreModule: Module get() = module { single { createDataStore(androidContext()) } } 
Enter fullscreen mode Exit fullscreen mode

iosMain (DataStoreModule.ios.kt)

actual val dataStoreModule: Module get() = module { single { createDataStore() } } 
Enter fullscreen mode Exit fullscreen mode

Notice in our Android implementation, we require a Context to create the Preferences DataStore, while in iOS, there is no context dependency.

To handle this difference, we can modify our Koin initialization function (initKoin) to accept a configuration function (config). This allows us to set up the Android-specific context without affecting iOS platform.

// commonMain fun initKoin( config: (KoinApplication.() -> Unit)? = null ) { startKoin { config?.invoke(this) modules(dataStoreModule) } } 
Enter fullscreen mode Exit fullscreen mode

Next, we need to initialize the Koin in Android and iOS

androidMain (Application Class)

class YourApplicationClass: Application() { override fun onCreate() { super.onCreate() initKoin( config = { androidContext(this@BaseApplication) } ) } } 
Enter fullscreen mode Exit fullscreen mode

iosMain (MainViewController.kt)

fun MainViewController() = ComposeUIViewController( configure = { initKoin() } ) { App() } 
Enter fullscreen mode Exit fullscreen mode

That’s it! Our Preferences DataStore is now ready to be used in a Repository or ViewModel.

Here’s an example of how you can use it:

ViewModel in **commonMain **package

class AppViewModel( private val dataStore: DataStore<Preferences> ): ViewModel() { private val key = stringPreferencesKey("name") private var _name = MutableStateFlow("") val name = _name.asStateFlow() init { viewModelScope.launch { dataStore.data.collect { storedData -> _name.update { storedData.get(key).orEmpty() } } } } fun updateName(name: String) = _name.update { name } fun storeToDataStore() { viewModelScope.launch { dataStore.updateData { it.toMutablePreferences().apply { set(key, name.value) } } } } } 
Enter fullscreen mode Exit fullscreen mode

Define a Koin Module in**commonMain **which provides a AppViewModel instance for dependency injection.

val viewModelModule = module { viewModel { AppViewModel(get()) } // automatically injecting the required parameters using get() } 
Enter fullscreen mode Exit fullscreen mode

Update our **initKoin **function:

fun initKoin( config: (KoinApplication.() -> Unit)? = null ) { startKoin { config?.invoke(this) modules(viewModelModule, dataStoreModule) // add viewModelModule } } 
Enter fullscreen mode Exit fullscreen mode

UI Layer (Shared UI with Jetpack Compose)

Inject AppViewModel in Composable App():

import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.Button import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.KoinContext import org.koin.compose.viewmodel.koinViewModel import org.koin.core.annotation.KoinExperimentalAPI @OptIn(KoinExperimentalAPI::class) @Composable @Preview fun App() { MaterialTheme { KoinContext { val viewModel = koinViewModel<AppViewModel>() val name by viewModel.name.collectAsStateWithLifecycle() Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Column( modifier = Modifier.padding(16.dp) ) { TextField( value = name, onValueChange = viewModel::updateName, label = { Text("Name") } ) Button(onClick = viewModel::storeToDataStore, modifier = Modifier.padding(top = 8.dp)) { Text("Store") } } } } } } 
Enter fullscreen mode Exit fullscreen mode

Voila! 🎉 The Preferences DataStore is now fully set up and ready to use in both Android and iOS. Now, let’s test it in action! Below is video showcasing how it works on both platforms.

Android API 35

Simulator 18.3.1

Want to see the full implementation? The complete code for this tutorial is available on my GitHub. Feel free to explore and try it out!
GitHub - rendyadidarma/DataStoreComposeMultiPlatform: A Compose multiplatform project that…

💬 Let’s stay connected! If you found this tutorial helpful, feel free to reach out and connect with me on LinkedIn or Github

Top comments (1)

Collapse
 
marlonlom profile image
Marlon López

Update datastore version from datastore = "1.1.3" to datastore = "1.1.5", some of the functions of the preference keys are gone