A Kotlin Multiplatform library that provides a thin, convenient wrapper around AndroidX DataStore Preferences and Proto DataStore.
Inspired by flow-preferences for SharedPreferences.
| Module | Description |
|---|---|
generic-datastore |
Core library with Preferences DataStore and Proto DataStore wrappers |
generic-datastore-compose |
Jetpack Compose extensions (remember(), rememberPreferences(), rememberBatchRead(), LocalPreferencesDatastore) |
Both modules target Android, Desktop (JVM), and iOS (iosX64, iosArm64,
iosSimulatorArm64).
Add the JitPack repository to your settings.gradle.kts:
dependencyResolutionManagement {
repositories {
maven("https://jitpack.io")
}
}Add the dependencies:
dependencies {
implementation("com.github.arthurkun:generic-datastore:<version>")
// Optional: Compose extensions
implementation("com.github.arthurkun:generic-datastore-compose:<version>")
}Add the GitHub Packages repository to your settings.gradle.kts. Authentication requires a GitHub
personal access token (classic) with the read:packages scope:
dependencyResolutionManagement {
repositories {
maven {
url = uri("https://maven.pkg.github.com/ArthurKun21/generic-datastore")
credentials {
username = providers.gradleProperty("gpr.user").orNull
?: System.getenv("GITHUB_ACTOR")
password = providers.gradleProperty("gpr.key").orNull
?: System.getenv("GITHUB_TOKEN")
}
}
}
}Set the credentials in your ~/.gradle/gradle.properties (or use the environment variables
GITHUB_ACTOR and GITHUB_TOKEN):
gpr.user=YOUR_GITHUB_USERNAME
gpr.key=YOUR_GITHUB_TOKENAdd the dependencies:
dependencies {
implementation("com.github.ArthurKun21:generic-datastore:<version>")
// Optional: Compose extensions
implementation("com.github.ArthurKun21:generic-datastore-compose:<version>")
}Use the createPreferencesDatastore factory function to create a GenericPreferencesDatastore:
val datastore = createPreferencesDatastore(
producePath = { context.filesDir.resolve("settings.preferences_pb").absolutePath },
)A fileName overload is available that appends the file name to a directory path:
val datastore = createPreferencesDatastore(
fileName = "settings.preferences_pb",
producePath = { context.filesDir.absolutePath },
)Optional parameters allow customizing the corruption handler, migrations, coroutine scope, and the
default Json instance used for serialization-based preferences:
val datastore = createPreferencesDatastore(
corruptionHandler = ReplaceFileCorruptionHandler { emptyPreferences() },
migrations = listOf(myMigration),
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
defaultJson = Json { prettyPrint = true },
producePath = { context.filesDir.resolve("settings.preferences_pb").absolutePath },
)Overloads accepting okio.Path and kotlinx.io.files.Path are also available:
val datastore = createPreferencesDatastore(
produceOkioPath = { "/data/settings.preferences_pb".toPath() },
)
val datastore = createPreferencesDatastore(
produceKotlinxIoPath = { Path("/data/settings.preferences_pb") },
)Alternatively, wrap an existing DataStore<Preferences> directly:
val datastore = GenericPreferencesDatastore(myExistingDataStore)The GenericPreferencesDatastore provides factory methods for all supported types:
val userName: Preference<String> = datastore.string("user_name", "Guest")
val userScore: Preference<Int> = datastore.int("user_score", 0)
val highScore: Preference<Long> = datastore.long("high_score", 0L)
val volume: Preference<Float> = datastore.float("volume", 1.0f)
val precision: Preference<Double> = datastore.double("precision", 0.0)
val darkMode: Preference<Boolean> = datastore.bool("dark_mode", false)
val tags: Preference<Set<String>> = datastore.stringSet("tags")Store enum values directly using the enum() extension:
enum class Theme { LIGHT, DARK, SYSTEM }
val themePref: Preference<Theme> = datastore.enum("theme", Theme.SYSTEM)Store any object by providing serializer/deserializer functions:
@Serializable
data class UserProfile(val id: Int, val email: String)
val userProfilePref: Preference<UserProfile> = datastore.serialized(
key = "user_profile",
defaultValue = UserProfile(0, ""),
serializer = { Json.encodeToString(UserProfile.serializer(), it) },
deserializer = { Json.decodeFromString(UserProfile.serializer(), it) },
)Or with a sealed class:
sealed class Animal(val name: String) {
data object Dog : Animal("Dog")
data object Cat : Animal("Cat")
companion object {
fun from(value: String): Animal = when (value) {
"Dog" -> Dog
"Cat" -> Cat
else -> throw Exception("Unknown animal type: $value")
}
fun to(animal: Animal): String = animal.name
}
}
val animalPref = datastore.serialized(
key = "animal",
defaultValue = Animal.Dog,
serializer = { Animal.to(it) },
deserializer = { Animal.from(it) },
)Store any @Serializable type directly without manual serializer/deserializer functions:
@Serializable
data class UserProfile(val name: String, val age: Int)
val userProfilePref: Preference<UserProfile> = datastore.kserialized(
key = "user_profile",
defaultValue = UserProfile(name = "John", age = 25),
)A custom Json instance can be provided if needed:
val customJson = Json { prettyPrint = true }
val userProfilePref: Preference<UserProfile> = datastore.kserialized(
key = "user_profile",
defaultValue = UserProfile(name = "John", age = 25),
json = customJson,
)Store a Set of custom objects using per-element serialization with serializedSet():
val animalSetPref: Preference<Set<Animal>> = datastore.serializedSet(
key = "animal_set",
defaultValue = emptySet(),
serializer = { Animal.to(it) },
deserializer = { Animal.from(it) },
)Each element is individually serialized to a String and stored using a stringSetPreferencesKey.
Elements that fail deserialization are silently skipped.
Store a Set of @Serializable objects without manual serializer/deserializer functions:
@Serializable
data class UserProfile(val name: String, val age: Int)
val profileSetPref: Preference<Set<UserProfile>> = datastore.kserializedSet(
key = "profile_set",
defaultValue = emptySet(),
)Each element is individually serialized to JSON and stored using a stringSetPreferencesKey.
Elements that fail deserialization are silently skipped. A custom Json instance can be provided if
needed.
Store a List of custom objects using per-element serialization with serializedList():
val animalListPref: Preference<List<Animal>> = datastore.serializedList(
key = "animal_list",
defaultValue = emptyList(),
serializer = { Animal.to(it) },
deserializer = { Animal.from(it) },
)Each element is individually serialized to a String and stored as a JSON array string using a
stringPreferencesKey. Elements that fail deserialization are silently skipped. Unlike sets, lists
preserve insertion order and allow duplicates.
Store a List of @Serializable objects without manual serializer/deserializer functions:
@Serializable
data class UserProfile(val name: String, val age: Int)
val profileListPref: Preference<List<UserProfile>> = datastore.kserializedList(
key = "profile_list",
defaultValue = emptyList(),
)The list is serialized to a JSON array string and stored using a stringPreferencesKey. If
deserialization fails (e.g., due to corrupted data), the default value is returned. A custom Json
instance can be provided if needed.
Store a Set of enum values with the enumSet() extension:
val themeSetPref: Preference<Set<Theme>> = datastore.enumSet<Theme>(
key = "theme_set",
defaultValue = emptySet(),
)Each enum value is stored by its name. Unknown enum values encountered during deserialization are
skipped.
Create preferences that return null when no value has been set, instead of a default value:
val nickname: Preference<String?> = datastore.nullableString("nickname")
val age: Preference<Int?> = datastore.nullableInt("age")
val timestamp: Preference<Long?> = datastore.nullableLong("timestamp")
val weight: Preference<Float?> = datastore.nullableFloat("weight")
val latitude: Preference<Double?> = datastore.nullableDouble("latitude")
val agreed: Preference<Boolean?> = datastore.nullableBool("agreed")
val labels: Preference<Set<String>?> = datastore.nullableStringSet("labels")Setting a nullable preference to null removes the key from DataStore. resetToDefault() also
removes the key, since the default is null.
nickname.get() // null (not set yet)
nickname.set("Alice") // stores "Alice"
nickname.get() // "Alice"
nickname.set(null) // removes the key
nickname.get() // nullStore an enum value that returns null when not set:
val themePref: Preference<Theme?> = datastore.nullableEnum<Theme>("theme")Store a nullable custom-serialized object:
val animalPref: Preference<Animal?> = datastore.nullableSerialized(
key = "animal",
serializer = { Animal.to(it) },
deserializer = { Animal.from(it) },
)Store a nullable @Serializable type:
@Serializable
data class UserProfile(val name: String, val age: Int)
val userProfilePref: Preference<UserProfile?> =
datastore.nullableKserialized<UserProfile>("user_profile")Store a nullable list of custom-serialized objects:
val animalListPref: Preference<List<Animal>?> = datastore.nullableSerializedList(
key = "animal_list",
serializer = { Animal.to(it) },
deserializer = { Animal.from(it) },
)Store a nullable list of @Serializable objects:
val profileListPref: Preference<List<UserProfile>?> =
datastore.nullableKserializedList<UserProfile>(
key = "profile_list",
)All nullable variants return null when the key is not set. Setting null removes the key. If
deserialization fails, null is returned.
Each Preference<T> provides multiple access patterns:
CoroutineScope(Dispatchers.IO).launch {
val name = userName.get()
userName.set("John Doe")
userName.delete()
}userName.asFlow().collect { value -> /* react to changes */ }
val nameState: StateFlow<String> = userName.stateIn(viewModelScope)// Blocks the calling thread — avoid on the main/UI thread
val name = userName.getBlocking()
userName.setBlocking("John Doe")DelegatedPreference<T> implements ReadWriteProperty, so you can use it as a delegated property:
var currentUserName: String by userName
// Read (blocking)
println(currentUserName)
// Write (blocking)
currentUserName = "Jane Doe"Atomically read-modify-write a preference value in a single DataStore transaction:
userScore.update { current -> current + 1 }Flip a Boolean preference:
darkMode.toggle()Toggle an item in a Set preference (adds if absent, removes if present):
tags.toggle("kotlin")// Suspend
userName.resetToDefault()
// Blocking — avoid on the main/UI thread
userName.resetToDefaultBlocking()Retrieve the underlying DataStore key name:
val key: String = userName.key()Batch operations let you read or write multiple preferences in a single DataStore transaction, avoiding redundant I/O and ensuring atomicity.
Read multiple preferences from a single snapshot:
class SettingsViewModel(
private val datastore: GenericPreferencesDatastore,
) : ViewModel() {
private val userName = datastore.string("user_name", "Guest")
private val darkMode = datastore.bool("dark_mode", false)
private val volume = datastore.float("volume", 1.0f)
fun loadSettings() {
viewModelScope.launch {
val (name, isDark, vol) = datastore.batchGet {
Triple(get[userName], get[darkMode], get[volume])
}
}
}
}Observe multiple preferences reactively from the same snapshot. The flow re-emits whenever any preference in the datastore changes:
class SettingsViewModel(
private val datastore: GenericPreferencesDatastore,
) : ViewModel() {
private val userName = datastore.string("user_name", "Guest")
private val darkMode = datastore.bool("dark_mode", false)
val settingsFlow: Flow<Pair<String, Boolean>> = datastore
.batchReadFlow{
get(userName) to get(darkMode)
}
.distinctUntilChanged()
}Write multiple preferences in a single atomic transaction:
class SettingsViewModel(
private val datastore: GenericPreferencesDatastore,
) : ViewModel() {
private val userName = datastore.string("user_name", "Guest")
private val darkMode = datastore.bool("dark_mode", false)
private val volume = datastore.float("volume", 1.0f)
fun resetSettings() {
viewModelScope.launch {
datastore.batchWrite {
set(userName, "Guest")
set(darkMode, false)
set(volume, 1.0f)
}
}
}
}Atomically read current values and write new values in a single transaction, guaranteeing consistency when new values depend on current ones:
class GameViewModel(
private val datastore: GenericPreferencesDatastore,
) : ViewModel() {
private val userScore = datastore.int("user_score", 0)
private val highScore = datastore.long("high_score", 0L)
fun submitScore(newScore: Int) {
viewModelScope.launch {
datastore.batchUpdate {
set(userScore, newScore)
val currentHigh = get(highScore)
if (newScore > currentHigh) {
set(highScore, newScore.toLong())
}
}
}
}
}The BatchUpdateScope also provides update, delete, and resetToDefault helpers:
datastore.batchUpdate {
update(userScore) { current -> current + 10 }
delete(nickname)
resetToDefault(volume)
}Blocking variants are available for non-coroutine contexts. Avoid calling these on the main/UI thread:
val (name, isDark) = datastore.batchGetBlocking {
get[userName] to get[darkMode]
}
datastore.batchWriteBlocking {
set(userName, "Guest")
set(darkMode, false)
}
datastore.batchUpdateBlocking {
update(userScore) { current -> current + 1 }
}Transform a Preference<T> into a Preference<R> with converter functions:
val scoreAsString: Preference<String> = userScore.map(
defaultValue = "0",
convert = { it.toString() },
reverse = { it.toIntOrNull() ?: 0 },
)
// Or infer the default value from the original preference's default:
val scoreAsString2: Preference<String> = userScore.mapIO(
convert = { it.toString() },
reverse = { it.toInt() },
)map catches exceptions in conversions and falls back to defaults. mapIO throws if conversion of
the default value fails.
GenericPreferencesDatastore supports exporting and importing all preferences using type-safe
backup models:
val backup: PreferencesBackup = datastore.exportAsData(
exportPrivate = false,
exportAppState = false,
)val jsonString: String = datastore.exportAsString(
exportPrivate = false,
exportAppState = false,
)A custom Json instance can be provided if needed:
val jsonString: String = datastore.exportAsString(
exportPrivate = false,
exportAppState = false,
json = customJson,
)datastore.importData(
backup = backup,
importPrivate = false,
importAppState = false,
)datastore.importDataAsString(
backupString = jsonString,
importPrivate = false,
importAppState = false,
)Import merges into existing preferences. Call datastore.clearAll() before import for full replace
semantics. A BackupParsingException is thrown if the JSON string is invalid or exceeds the 10 MB
size limit.
Use BasePreference.privateKey(key) or BasePreference.appStateKey(key) to prefix keys so they can
be filtered during backup:
val token = datastore.string(BasePreference.privateKey("auth_token"), "")
val onboarded = datastore.bool(BasePreference.appStateKey("onboarding_done"), false)Use the createProtoDatastore factory function to create a GenericProtoDatastore:
val protoDatastore = createProtoDatastore(
serializer = MyProtoSerializer,
defaultValue = MyProtoMessage.getDefaultInstance(),
producePath = { context.filesDir.resolve("my_proto.pb").absolutePath },
)A fileName overload is available that appends the file name to a directory path:
val protoDatastore = createProtoDatastore(
serializer = MyProtoSerializer,
defaultValue = MyProtoMessage.getDefaultInstance(),
fileName = "my_proto.pb",
producePath = { context.filesDir.absolutePath },
)Optional parameters allow customizing the key, corruption handler, migrations, coroutine scope, and
the default Json instance used for serialization-based field preferences:
val protoDatastore = createProtoDatastore(
serializer = MyProtoSerializer,
defaultValue = MyProtoMessage.getDefaultInstance(),
key = "my_proto",
corruptionHandler = ReplaceFileCorruptionHandler { MyProtoMessage.getDefaultInstance() },
migrations = listOf(myMigration),
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
defaultJson = Json { prettyPrint = true },
producePath = { context.filesDir.resolve("my_proto.pb").absolutePath },
)Overloads accepting okio.Path and kotlinx.io.files.Path are also available:
val protoDatastore = createProtoDatastore(
serializer = MyProtoSerializer,
defaultValue = MyProtoMessage.getDefaultInstance(),
produceOkioPath = { "/data/my_proto.pb".toPath() },
)
val protoDatastore = createProtoDatastore(
serializer = MyProtoSerializer,
defaultValue = MyProtoMessage.getDefaultInstance(),
produceKotlinxIoPath = { Path("/data/my_proto.pb") },
)Alternatively, wrap an existing DataStore<T> directly:
val protoDatastore = GenericProtoDatastore(
datastore = myExistingProtoDataStore,
defaultValue = MyProtoMessage.getDefaultInstance(),
)val dataPref: ProtoPreference<MyProtoMessage> = protoDatastore.data()
// Then use get(), set(), asFlow(), etc. just like Preferences DataStoreUse field() to create a preference for an individual field of the proto message. The getter
extracts the field value from a snapshot, and updater returns a new proto with the field updated:
val namePref: ProtoPreference<String> = protoDatastore.field(
defaultValue = "",
getter = { it.name },
updater = { proto, value -> proto.copy(name = value) },
)
// Suspend
namePref.set("Alice")
val name = namePref.get() // "Alice"
// Flow
namePref.asFlow().collect { value -> /* react to changes */ }
// Blocking
namePref.setBlocking("Bob")
val blocking = namePref.getBlocking() // "Bob"
// Delegation
var userName: String by namePrefFor nested fields, chain copy() calls in the updater:
data class Address(val street: String = "", val city: String = "")
data class Profile(val nickname: String = "", val address: Address = Address())
data class Settings(val id: Int = 0, val profile: Profile = Profile())
// Access a deeply nested field
val cityPref: ProtoPreference<String> = protoDatastore.field(
defaultValue = "",
getter = { it.profile.address.city },
updater = { proto, value ->
proto.copy(
profile = proto.profile.copy(
address = proto.profile.address.copy(city = value),
),
)
},
)For nullable nested fields, provide fallback defaults in the updater:
data class NullableProfile(val nickname: String = "", val age: Int? = null)
data class NullableSettings(val id: Int = 0, val profile: NullableProfile? = null)
val agePref: ProtoPreference<Int?> = protoDatastore.field(
defaultValue = null,
getter = { it.profile?.age },
updater = { proto, value ->
proto.copy(
profile = (proto.profile ?: NullableProfile()).copy(age = value),
)
},
)Field preferences share the same underlying DataStore, so changes through field() are visible
via data() and vice versa. delete() and resetToDefault() on a field reset only the targeted
field to its default value (via set(defaultValue) → updater(current, fieldDefault)). It does
not reset the entire proto to its default.
Store an enum value in a proto String field using enumField():
enum class Theme { LIGHT, DARK, SYSTEM }
val themePref: ProtoPreference<Theme> = protoDatastore.enumField(
defaultValue = Theme.SYSTEM,
getter = { it.theme },
updater = { proto, value -> proto.copy(theme = value) },
)The reified overload infers enumValues automatically. Unknown enum names encountered during
deserialization fall back to the default value.
Store a nullable enum field using nullableEnumField():
val themePref: ProtoPreference<Theme?> = protoDatastore.nullableEnumField<Settings, Theme>(
getter = { it.theme },
updater = { proto, value -> proto.copy(theme = value) },
)Returns null when the proto field is null. Unknown enum names also return null.
Store a Set of enum values backed by a Set<String> proto field:
val themesPref: ProtoPreference<Set<Theme>> = protoDatastore.enumSetField<Settings, Theme>(
defaultValue = emptySet(),
getter = { it.themes },
updater = { proto, value -> proto.copy(themes = value) },
)Each enum value is stored by its name. Unknown enum names are skipped during deserialization.
Store a custom-serialized object in a proto String field:
@Serializable
data class UserProfile(val id: Int, val email: String)
val profilePref: ProtoPreference<UserProfile> = protoDatastore.serializedField(
defaultValue = UserProfile(0, ""),
serializer = { Json.encodeToString(UserProfile.serializer(), it) },
deserializer = { Json.decodeFromString(UserProfile.serializer(), it) },
getter = { it.profileJson },
updater = { proto, value -> proto.copy(profileJson = value) },
)If the raw string is blank or deserialization fails, the default value is returned.
Store a nullable custom-serialized object:
val profilePref: ProtoPreference<UserProfile?> = protoDatastore.nullableSerializedField(
serializer = { Json.encodeToString(UserProfile.serializer(), it) },
deserializer = { Json.decodeFromString(UserProfile.serializer(), it) },
getter = { it.profileJson },
updater = { proto, value -> proto.copy(profileJson = value) },
)Returns null when the proto field is null. Setting null writes null to the proto field.
Store any @Serializable type in a proto String field using Kotlin Serialization directly:
val profilePref: ProtoPreference<UserProfile> = protoDatastore.kserializedField(
defaultValue = UserProfile(0, ""),
getter = { it.profileJson },
updater = { proto, value -> proto.copy(profileJson = value) },
)The reified overload infers KSerializer automatically. A custom Json instance can be provided
if needed.
Store a nullable @Serializable type:
val profilePref: ProtoPreference<UserProfile?> =
protoDatastore.nullableKserializedField<Settings, UserProfile>(
getter = { it.profileJson },
updater = { proto, value -> proto.copy(profileJson = value) },
)Store a List of custom objects using per-element serialization in a proto String field:
val animalListPref: ProtoPreference<List<Animal>> = protoDatastore.serializedListField(
defaultValue = emptyList(),
elementSerializer = { Animal.to(it) },
elementDeserializer = { Animal.from(it) },
getter = { it.animalsJson },
updater = { proto, value -> proto.copy(animalsJson = value) },
)Each element is individually serialized and stored as a JSON array string.
Store a nullable list of custom-serialized objects:
val animalListPref: ProtoPreference<List<Animal>?> = protoDatastore.nullableSerializedListField(
elementSerializer = { Animal.to(it) },
elementDeserializer = { Animal.from(it) },
getter = { it.animalsJson },
updater = { proto, value -> proto.copy(animalsJson = value) },
)Store a List of @Serializable objects using Kotlin Serialization:
val profileListPref: ProtoPreference<List<UserProfile>> = protoDatastore.kserializedListField(
defaultValue = emptyList(),
getter = { it.profilesJson },
updater = { proto, value -> proto.copy(profilesJson = value) },
)Store a nullable list of @Serializable objects:
val profileListPref: ProtoPreference<List<UserProfile>?> =
protoDatastore.nullableKserializedListField<Settings, UserProfile>(
getter = { it.profilesJson },
updater = { proto, value -> proto.copy(profilesJson = value) },
)Store a Set of custom objects using per-element serialization backed by a Set<String> proto
field:
val animalSetPref: ProtoPreference<Set<Animal>> = protoDatastore.serializedSetField(
defaultValue = emptySet(),
serializer = { Animal.to(it) },
deserializer = { Animal.from(it) },
getter = { it.animalNames },
updater = { proto, value -> proto.copy(animalNames = value) },
)Elements that fail deserialization are silently skipped.
Store a Set of @Serializable objects with per-element JSON serialization backed by a
Set<String> proto field:
val profileSetPref: ProtoPreference<Set<UserProfile>> = protoDatastore.kserializedSetField(
defaultValue = emptySet(),
getter = { it.profileNames },
updater = { proto, value -> proto.copy(profileNames = value) },
)Elements that fail deserialization are silently skipped. A custom Json instance can be provided
if needed.
The remember() extension turns any DelegatedPreference<T> into a lifecycle-aware
MutableState<T>:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
@Composable
fun SettingsScreen(datastore: GenericPreferencesDatastore) {
var userName by datastore.string("user_name", "Guest").remember()
Column {
Text("Current User: $userName")
OutlinedTextField(
value = userName,
onValueChange = { userName = it },
label = { Text("Enter username") },
)
}
}Under the hood, remember() uses collectAsStateWithLifecycle for lifecycle-safe collection for
Android while it uses collectAsState() for Desktop/JVM. It launches a coroutine for writes.
An optimistic local override is applied immediately so that synchronous UI inputs reflect the new
value without waiting for the DataStore round-trip.
Collects a batchReadFlow as a Compose State, reading multiple preferences from a single
DataStore snapshot:
@Composable
fun SettingsScreen(datastore: GenericPreferencesDatastore) {
val userName = datastore.string("user_name", "Guest")
val darkMode = datastore.bool("dark_mode", false)
val settings by datastore.rememberBatchRead {
get(userName) to get(darkMode)
}
settings?.let { (name, isDark) ->
Text("User: $name, Dark mode: $isDark")
}
}The returned State is null until the first snapshot is available.
Remembers multiple preferences (2–5) as individual MutableState values, reading from a shared
batchReadFlow snapshot and writing via batchWrite. All reads share a single DataStore
observation, and writes are launched asynchronously with an optimistic local override:
@Composable
fun SettingsScreen(datastore: GenericPreferencesDatastore) {
val userName = datastore.string("user_name", "Guest")
val darkMode = datastore.bool("dark_mode", false)
val volume = datastore.float("volume", 1.0f)
val (name, isDark, vol) = datastore.rememberPreferences(userName, darkMode, volume)
Column {
var nameValue by name
OutlinedTextField(
value = nameValue,
onValueChange = { nameValue = it },
label = { Text("Username") },
)
var isDarkValue by isDark
Switch(checked = isDarkValue, onCheckedChange = { isDarkValue = it })
}
}Overloads are available for 2, 3, 4, and 5 preferences. The returned PreferencesStateN also
supports property delegation:
val prefs by datastore.rememberPreferences(userName, darkMode)
Text("User: ${prefs.first}, Dark: ${prefs.second}")
prefs.first = "New Name" // triggers an async writeUse ProvidePreferencesDatastore to supply a PreferencesDatastore via CompositionLocal, then
call the standalone rememberPreferences overloads without an explicit datastore receiver:
@Composable
fun App(datastore: GenericPreferencesDatastore) {
ProvidePreferencesDatastore(datastore) {
SettingsScreen()
}
}
@Composable
fun SettingsScreen() {
val userName = LocalPreferencesDatastore.current.string("user_name", "Guest")
val darkMode = LocalPreferencesDatastore.current.bool("dark_mode", false)
val (name, isDark) = rememberPreferences(userName, darkMode)
var nameValue by name
var isDarkValue by isDark
Column {
OutlinedTextField(
value = nameValue,
onValueChange = { nameValue = it },
label = { Text("Username") },
)
Switch(checked = isDarkValue, onCheckedChange = { isDarkValue = it })
}
}Accessing LocalPreferencesDatastore without a provider throws an IllegalStateException.