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)
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" }
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)
}
}
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"
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
}
}
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"
}
}
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
Add the actual declaration in androidMain and iosMain :
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()) } }
iosMain (DataStoreModule.ios.kt)
actual val dataStoreModule: Module
get() = module { single { createDataStore() } }
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)
}
}
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)
}
)
}
}
iosMain (MainViewController.kt)
fun MainViewController() = ComposeUIViewController(
configure = {
initKoin()
}
) { App() }
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)
}
}
}
}
}
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()
}
Update our **initKoin **function:
fun initKoin(
config: (KoinApplication.() -> Unit)? = null
) {
startKoin {
config?.invoke(this)
modules(viewModelModule, dataStoreModule) // add viewModelModule
}
}
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")
}
}
}
}
}
}
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.
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 (0)