如何将 DataStore 与 StateFlow 和 Jetpack Compose 一起使用?

How to use DataStore with StateFlow and Jetpack Compose?

我尝试让用户选择是使用 UI 模式 'light'、'dark' 还是 'system' 设置。我想将选择保存为 DataStore。

用户选择的下拉菜单未从 DataStore 加载值。加载屏幕时总是显示 stateIn() 的初始值。

设置管理器:

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)
        }
    }
}

视图模型:

@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)
        }
    }
}

撰写:

@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)
                    }
                }
            }
        }
    }
}

为什么没有为 currentUiMode 更新值? 我不想为此使用 LiveData。

如果有人在寻找 Compose DataStore 包装器时找到了这个答案,请查看 this answer


在 Compose 中唯一可以导致重组的是更改 State 对象。

简单地发射到一个流不会那样做。您可以使用 collectAsState 收集流,它是从 FlowState 的映射器。使用 Flow 你需要一个默认值,因为它没有当前的 value,但是使用 StateFlow 你不需要那个。

您的代码中的另一个问题是 remember { mutableStateOf... 仅记住第一个值,selectedText 不会被 selectedItemIndex 更新。通常,您可以将它作为参数传递给 remember,或使用 derivedStateOf,但在这种特殊情况下根本不需要使用 remember&mutableStateOf,因为在 optionList 的情况下,因为这些是静态值并且 selectedItemIndex 不会经常更新。

remember&mutableStateOf 只应在需要更改某些具有副作用的值时使用,例如单击按钮。请参阅 for how that works. You can also use remember without mutableStateOf if you don't want to repeat calculations of medium severity - don't do really heavy calculations without side effects 或后台线程。

因此以下内容应该适合您:

var expanded by remember { mutableStateOf(false) }

val selectedItemIndex by settingsViewModel.currentUiMode.collectAsState()
var selectedText = if (selectedItemIndex == null) "" else items[selectedItemIndex]
val optionList: Map<Int, String> = settingsViewModel.selectableUiModes