I try to give the user the choice whether to use the UI Mode 'light', 'dark' or the 'system' settings. I would like to save the selection as DataStore.
The drop down menu for the user selection is not loading the value from DataStore. It is always showing the initialvalue from stateIn() when loading the screen.
SettingsManager:
val Context.dataStoreUiSettings: DataStore<Preferences> by preferencesDataStore(name = DATA_STORE_UI_NAME)
object PreferencesKeys {
val UI_MODE: Preferences.Key<Int> = intPreferencesKey("ui_mode")
}
class SettingsManager @Inject constructor(private val context: Context) { //private val dataStore: DataStore<Preferences>
private val TAG: String = "UserPreferencesRepo"
// Configuration.UI_MODE_NIGHT_UNDEFINED, Configuration.UI_MODE_NIGHT_YES, Configuration.UI_MODE_NIGHT_NO
suspend fun setUiMode(uiMode: Int) {
context.dataStoreUiSettings.edit { preferences ->
preferences[UI_MODE] = uiMode
}
}
fun getUiMode(key: Preferences.Key<Int>, default: Int): Flow<Int> {
return context.dataStoreUiSettings.data
.catch { exception ->
if (exception is IOException) {
Timber.i("Error reading preferences: $exception")
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preference ->
preference[key] ?: default
}
}
fun <T> getDataStore(key: Preferences.Key<T>, default: Any): Flow<Any> {
return context.dataStoreUiSettings.data
.catch { exception ->
if (exception is IOException) {
Timber.i("Error reading preferences: $exception")
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preference ->
preference[key] ?: default
}
}
suspend fun clearDataStore() {
context.dataStoreUiSettings.edit { preferences ->
preferences.clear()
}
}
suspend fun removeKeyFromDataStore(key: Preferences.Key<Any>) {
context.dataStoreUiSettings.edit { preference ->
preference.remove(key)
}
}
}
ViewModel:
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val settingsUseCases: SettingsUseCases,
private val settingsManager: SettingsManager,
) : ViewModel() {
private val _selectableUiModes = mapOf(
UI_MODE_NIGHT_UNDEFINED to "System",
UI_MODE_NIGHT_NO to "Light",
UI_MODE_NIGHT_YES to "Dark"
)
val selectableUiModes = _selectableUiModes
val currentUiMode: StateFlow<Int?> = settingsManager.getUiMode(UI_MODE, UI_MODE_NIGHT_UNDEFINED).stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = null,
)
init {
Timber.i("SettingsViewModel created")
}
fun setUiMode(uiModeKey: Int) {
viewModelScope.launch(Dispatchers.IO) {
settingsManager.setUiMode(uiModeKey)
}
}
}
Compose:
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun OutlinedDropDown(
modifier: Modifier = Modifier,
readOnly: Boolean = true,
isEnabled: Boolean = true,
isError: Boolean = false,
settingsViewModel: SettingsViewModel = hiltViewModel(),
) {
val items: Map<Int, String> = settingsViewModel.selectableUiModes
var expanded by remember { mutableStateOf(false) }
val selectedItemIndex = settingsViewModel.currentUiMode
var selectedText by remember { mutableStateOf(if (selectedItemIndex.value == null) "" else items[selectedItemIndex.value]) }
val optionList by remember { mutableStateOf(items) }
Column {
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !expanded
}
) {
OutlinedTextField(
isError = isError,
enabled = isEnabled,
modifier = modifier,
readOnly = readOnly,
value = selectedText!!,
onValueChange = {
selectedText = it
},
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
}
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
optionList.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
selectedText = selectionOption.value
settingsViewModel.setUiMode(selectionOption.key)
expanded = false
}
) {
Text(text = selectionOption.value)
}
}
}
}
}
}
Why is the value not getting updated for currentUiMode? I don't want to use LiveData for it.
If anyone found this answer when looking for the Compose DataStore wrapper, check out this answer.
The only thing that can cause recomposition in Compose is changing the
State
object.Simply emitting to a flow will not do that. You can collect the flow using
collectAsState
, which is a mapper fromFlow
toState
. WithFlow
you need a default value, as it doesn't have currentvalue
, but withStateFlow
you don't need that.Another problem in your code is that with
remember { mutableStateOf...
only the first value is remembered,selectedText
will not be updated withselectedItemIndex
. Generally, you could pass it as a parameter toremember
, or usederivedStateOf
, but in this particular case there is no need to useremember
&mutableStateOf
at all, as in the case ofoptionList
, since these are static values andselectedItemIndex
is not gonna be updated any often.remember
&mutableStateOf
should only be used when you need to change some value with a side effect, such as a button click. See this answer for how that works. You can also useremember
withoutmutableStateOf
if you don't want to repeat calculations of medium severity - don't do really heavy calculations without side effects or background thread.So the following should work for you: