使用 Android Jetpack Compose 构建首选项屏幕

问题描述 投票:0回答:3

我必须构建一个新的 Android 应用程序。由于 Jetpack Compose 现已稳定,我想用它构建整个 UI。此外,我还需要一个首选项/设置屏幕,用户可以在其中指定他的首选项。根据文档,仍然建议通过 Fragments 执行此操作。 https://developer.android.com/guide/topics/ui/settings

我还发现一个外部库可以以撰写方式提供此功能。 https://github.com/alorma/Compose-Settings

以前有人这样做过吗? “最干净”的方法是什么?

谢谢你

android android-jetpack android-jetpack-compose
3个回答
9
投票

我已经在 compose 中实现了一个应用程序,并使用 Jetpack Compose 中提供的工具创建了设置屏幕,没有片段。

我这样做的方法是为每个设置选项创建一个可组合项,其中包含标题、可选副标题和一个复选框,该复选框指示该选项是否启用。

然后将这些选项添加到列(或网格,如果您有平板电脑,则很容易在撰写中支持两者);您只需将复选框上的点击挂接到视图模型即可更改设置,然后刷新 UI。

如果您有其他类型的设置条目,您也可以为这些条目定义您自己的可组合项。就我而言,我有其他设置选项,可以打开一个对话框,用户可以在其中配置一些参数,我还有另一个用于此类设置行的可组合函数。

这是个人意见,但我更喜欢远离第三方库,除非a)它们提供了显着的价值,b)它们来自提供某种维护和错误修复保证的来源。我不知道你提到的那个库,但如果你包含第 3 方库,你需要意识到你正在放弃对应用程序部分的控制,所以你需要平衡成本/效益比。


0
投票

我也想知道同样的事情。我发现在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() }

源代码


0
投票

我创建了一个实现 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。

© www.soinside.com 2019 - 2024. All rights reserved.