如何将 DataStore 与 StateFlow 和 Jetpack Compose 结合使用?

Ohl*_*sen 7 kotlin android-jetpack-compose android-jetpack-datastore kotlin-stateflow

我尝试让用户选择是否使用 UI 模式“浅色”、“深色”或“系统”设置。我想将选择保存为数据存储。

用户选择的下拉菜单未从数据存储加载值。加载屏幕时,它始终显示 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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

视图模型:

@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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

撰写:

@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)
                    }
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Phi*_*hov 2

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


在 Compose 中唯一可能导致重组的事情是更改对象State

简单地发送到流并不能做到这一点。您可以使用 收集流量,它是tocollectAsState的映射器。With你需要一个默认值,因为它没有 current ,但你不需要它。FlowStateFlowvalueStateFlow

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

remembermutableStateOf仅当您需要更改某些具有副作用的值(例如单击按钮)时才应使用& 。请参阅此答案了解其工作原理。如果您不想重复中等严重程度的计算,也可以使用rememberwithout - 不要在没有副作用或后台线程的情况下进行真正繁重的计算。mutableStateOf

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

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
Run Code Online (Sandbox Code Playgroud)