如何将 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
收集流,它是从 Flow
到 State
的映射器。使用 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
我尝试让用户选择是使用 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
收集流,它是从 Flow
到 State
的映射器。使用 Flow
你需要一个默认值,因为它没有当前的 value
,但是使用 StateFlow
你不需要那个。
您的代码中的另一个问题是 remember { mutableStateOf...
仅记住第一个值,selectedText
不会被 selectedItemIndex
更新。通常,您可以将它作为参数传递给 remember
,或使用 derivedStateOf
,但在这种特殊情况下根本不需要使用 remember
&mutableStateOf
,因为在 optionList
的情况下,因为这些是静态值并且 selectedItemIndex
不会经常更新。
remember
&mutableStateOf
只应在需要更改某些具有副作用的值时使用,例如单击按钮。请参阅 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