我必须构建一个新的 Android 应用程序。由于 Jetpack Compose 现已稳定,我想用它构建整个 UI。此外,我还需要一个首选项/设置屏幕,用户可以在其中指定他的首选项。根据文档,仍然建议通过 Fragments 执行此操作。 https://developer.android.com/guide/topics/ui/settings
我还发现一个外部库可以以撰写方式提供此功能。 https://github.com/alorma/Compose-Settings
以前有人这样做过吗? “最干净”的方法是什么?
谢谢你
我已经在 compose 中实现了一个应用程序,并使用 Jetpack Compose 中提供的工具创建了设置屏幕,没有片段。
我这样做的方法是为每个设置选项创建一个可组合项,其中包含标题、可选副标题和一个复选框,该复选框指示该选项是否启用。
然后将这些选项添加到列(或网格,如果您有平板电脑,则很容易在撰写中支持两者);您只需将复选框上的点击挂接到视图模型即可更改设置,然后刷新 UI。
如果您有其他类型的设置条目,您也可以为这些条目定义您自己的可组合项。就我而言,我有其他设置选项,可以打开一个对话框,用户可以在其中配置一些参数,我还有另一个用于此类设置行的可组合函数。
这是个人意见,但我更喜欢远离第三方库,除非a)它们提供了显着的价值,b)它们来自提供某种维护和错误修复保证的来源。我不知道你提到的那个库,但如果你包含第 3 方库,你需要意识到你正在放弃对应用程序部分的控制,所以你需要平衡成本/效益比。
我也想知道同样的事情。我发现在codelab中使用存储库有点令人困惑。我想要一种简单的方法来定义一次键和默认值。
我的方法是拥有一个界面并使用
DataStore
提供 CompositionLocalProvider
(受 React.js 启发,不确定这是否被认为是最佳实践)。
/**
* Convenient methods to get data from a DataStore
*/
interface Preference<V> {
/**
* Key to get a preference
*/
val key: Preferences.Key<V>
/**
* A context is necessary to have a default value that matches with the current theme or language
*/
val defaultValueFromContext: Context.() -> V
// value
fun value(preferences: Preferences): V? = preferences[key]
fun valueOrDefault(preferences: Preferences, context: Context): V =
value(preferences) ?: defaultValueFromContext(context)
// read
fun flow(dataStore: DataStore<Preferences>): Flow<V?> =
dataStore.data.map { value(it) }.distinctUntilChanged()
fun flowOrDefault(dataStore: DataStore<Preferences>, context: Context): Flow<V> =
dataStore.data.map { valueOrDefault(it, context) }.distinctUntilChanged()
// write
suspend fun edit(dataStore: DataStore<Preferences>, newValue: (currentValue: V?) -> V) {
dataStore.edit {
it[key] = newValue(value(it))
}
}
}
/**
* CompositionLocal providing a datastore
*/
val LocalDataStore = compositionLocalOf { dummyDataStore } // TODO use static?
@Composable
fun DataStoreProvider(
dataStore: DataStore<Preferences> = LocalContext.current.settingsDataStore,
content: @Composable () -> Unit
) =
CompositionLocalProvider(LocalDataStore.provides(dataStore)) { content() }
我没有使用 ViewModel,因为我不认为每个屏幕有多个视图模型(针对每个首选项)被认为是最佳实践,而且我没有找到一种方法来为可变数量的首选项使用单个 ViewModel 。所以,我只是使用扩展功能。
/**
* Avoid having to specify an initial value
*/
@Composable
fun <V> Preference<V>.stateOrDefault() =
flowOrDefault(LocalDataStore.current, LocalContext.current).collectAsState(initial = defaultValue()) // TODO with lifecycle
@Composable
fun <V> Preference<V>.defaultValue() = defaultValueFromContext(LocalContext.current)
打好基础之后,做一些诸如更改主题之类的事情就相对容易了。
AppTheme(darkTheme = DARK_THEME.stateOrDefault().value) { }
class DarkThemePreference(keyName: String = "dark_theme") : BooleanPreference(keyName) {
override val defaultValueFromContext: Context.() -> Boolean
get() = {
isSystemInDarkTheme(resources.configuration)
}
}
fun isSystemInDarkTheme(configuration: Configuration): Boolean {
return (configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
}
@Composable
fun SwitchPreference(
modifier: Modifier = Modifier,
title: String,
preference: Preference<Boolean>,
description: String? = null,
icon: ComposeFunction? = null
) {
var checked by preference.optimisticState()
PreferenceItem(
modifier = modifier,
title = title,
description = description,
icon = icon,
onClick = { checked = !checked },
action = {
Switch(checked = checked, onCheckedChange = { checked = it })
})
}
对于主题,我还使用了
CompositionLocal
。
// make sure values are divisible by 4 for consistency
val Int.unit: Dp
get() = this * 4.dp
val Int.bigUnit: Dp
get() = 4.unit
/**
* Values for placing and spacing elements
*/
data class Spacing(
val itemMinHeight: Dp = 3.bigUnit,
val itemPadding: PaddingValues = PaddingValues(horizontal = 1.bigUnit, vertical = 2.unit),
val iconPadding: PaddingValues = PaddingValues(end = 5.unit),
val actionPadding: PaddingValues = PaddingValues(start = 1.unit),
val dividerPadding: PaddingValues = PaddingValues(bottom = 4.bigUnit)
)
val LocalSpacing = compositionLocalOf { Spacing() }
我创建了一个实现 Jetpack Compose Material 3 首选项的库,其设计时考虑到了可扩展性和易用性:
https://github.com/zhanghai/ComposePreference
您可以简单地使用它:
ProvidePreferenceLocals {
LazyColumn(modifier = Modifier.fillMaxSize()) {
switchPreference(
key = "switch_preference",
defaultValue = false,
title = { Text(text = "Switch preference") },
icon = { Icon(imageVector = Icons.Outlined.Info, contentDescription = null) },
summary = { Text(text = if (it) "On" else "Off") }
)
}
}
或者在这个库中探索其他更具可扩展形式的 API。