【译】Jetpack Compose State 指南

原文:https://medium.com/@takahirom/jetpack-compose-state-guideline-494d467b6e76

官方文档中有很多指南,但并不是 DO DON'T 格式指南。所以我想把它们总结一下,以方便 Compose 的开发。

如果您有任何反馈,请告诉我。我想修改一下这篇文章。

参考

Android 开发者 https://developer.android.google.cn/jetpack/compose/state

Compose state 心得:使用 Jetpack Compose 的自动状态观察 https://www.youtube.com/watch?v=rmv2ug-wW4U

Codelab https://developer.android.google.cn/codelabs/jetpack-compose-state#0

Compose 基础知识

本节不是 Compose 指南,而是对 Compose 基本用法的解释,这是使用 Compose 实现应用程序时所必需的。

1:UI 状态的变化必须能够被 Compose 观察到。

首先 State 是什么?

应用程序中的状态是可以随时间变化的任何值。这是一个非常广泛的定义,涵盖从 Room 数据库到类变量的所有内容。

https://developer.android.google.cn/jetpack/compose/state?hl=en

如果应用程序是新闻应用程序,则文章就是状态。如果应用程序有开关,则状态是开关是否打开。基本上,大多数应用程序都使用“State”。

为了让 Compose 观察到 State 的变化,需要使用 MutableState 或 collectAsState() 来使用 Compose 的 State 而不是 Kotlin 的 var 变量。

在 Compose 中使用 Kotlin 的 var 声明变量效果不佳,因为 Compose 无法观察到更改。

❌DON’T

@Composable
fun MySwitch() {// I have a Kotlin variable with a State of check.var checked = falseSwitch(
       ​checked = checked,
       ​onCheckedChange = {
           ​checked = it
       ​})
}

请添加图片描述
更改不会得到反映。不能当开关用啊😂

⭕️ DO

@Composable
fun MySwitch() {// I have a State in Compose's MutableStateval checked = remember { mutableStateOf(false) }Switch(// Observe the State by accessing the value property
       ​checked = checked.value,
       ​onCheckedChange = {
           checked.value = it
       ​})
}

请添加图片描述

为什么
因为状态变化没有被反映并且不起作用。

为什么 DO 示例效果这么好?

实际上,当状态改变时,MySwitch 的DO函数会被重组(重新执行) 。然而,DON'T函数没有被重组。
让我们添加 println() 并看看 DO 示例是否真的能再次工作。

DO 例子

@Composable
fun MySwitch() {// MutableStateval checked = remember { mutableStateOf(false) }// ****↓Add****println("MySwitch(): $checked")// ****↑Add****Switch(
       ​checked = checked.value,
       ​onCheckedChange = {
           ​checked.value = it
       ​})
}

您可以看到 MySwitch() 重新运行,如下所示。
请添加图片描述

为什么 MySwitch() 会重新运行?

您可能想知道的下一个问题是它是如何重新执行的,对吧?

如果您转到 MutableState 实现,您将看到以下实现: 在 Jetpack Compose 中,State 值被设置为自定义 setter 或自定义 getter,这意味着它不仅仅是一个变量,而是经过处理的。
请添加图片描述
来自 SnapshotMutableStateImpl

换句话说,Jetpack Compose 会监视 State 值的读取,并且 Jetpack Compose 保留了读取 MutableState 值的函数,因此它可以在 MutableState 更改时调用读取 State 的函数。
请添加图片描述
该图显示 Compose 运行时知道MySwitch()获得了 MutableState.value。

2 在 Composable 中创建的状态必须被 remember

您需要对可组合函数中创建的状态使用remember{}
使用remember{}可以让您将数据保留在 Compose 中。

❌DON’T
更改不会像前面的示例中那样反映出来。

@Composable
fun MySwitch() {// remember{} is not used.var checked = mutableStateOf(false)Switch(
       ​checked = checked.value,
       ​onCheckedChange = {
           ​checked.value = it
       ​})
}

⭕️ DO

@Composable
fun MySwitch() {// remember{} is used.var checked = remember { mutableStateOf(false) }Switch(
       ​checked = checked.value,
       ​onCheckedChange = {
           ​checked.value = it
       ​})
}

为什么

再次,让我们通过将println()”放入MySwitch()来看看MySwitch()在 DON’T 示例中是否正常工作。

@Composable
fun MySwitch() {val checked = mutableStateOf(false)println("MySwitch(): $checked")Switch(
       ​checked = checked.value,
       ​onCheckedChange = {
           ​checked.value = it
       ​})
}

请添加图片描述

看来 MySwitch() 正在被重组。但是,MutableState 的值没有改变。

使用 MutableState 并检测到更改并重新组合函数,但是如果不使用 remember,则不会保留在 Composable 中创建的状态,因此创建了一个新的 MutableState(false) 并且不保留状态。
可以看到,每次实例的 HashCode 发生变化,MutableState 都会重新创建

请添加图片描述

当我 remember{} 时它存储在哪里?

要理解这一点,您需要首先了解Composition
Compose 通过在运行时执行 Composable 函数来组合 Composition。
Composition 是一个树形结构,例如下面的Composable 函数将组成一个如下图所示的Composition。

@Composable
 fun MyComposable() { 
   Column {Text("Hello")Text("World")} 
}

请添加图片描述

该 Composition 不仅可以保存 UI 元素,还可以保存通过remember{}保存的数据。

让我们回到我们的例子,如果这个 Composition 不被 remember 的话会是什么样子?

@Composable
fun MySwitch() {// remember{} is not used.var checked = mutableStateOf(false)Switch(
       ​checked = checked.value,
       ​onCheckedChange = {
           ​checked.value = it
       ​})
}

在以下示例中,有一个 Switch() 作为 MySwitch() 的子级。
MutableState 不存储在 Composition 中。

MySwitch
└── Switch

那么如果您使用remember{}会发生什么?

MySwitch
├── **MutableState (var checked)**
└── Switch

如果您以这种方式使用remember{},则 MutableState 会保存在此 Composition 中。由于 remember{} 保存了 MySwitch 进入 Composition 时的值,并在下一次和后续调用中使用它,因此它被保存和使用。

https://developer.android.google.cn/jetpack/compose/state#state-in-composables

可组合函数可以使用 remember 可组合项将单个对象存储在内存中。在初始组合期间,由 remember 计算出的值存储在组合中,并且在重组期间返回存储的值。remember 可用于存储可变和不可变对象。

3:让我们使用 Kotlin 的委托属性来访问状态。

❌DON’T

@Composable
fun MySwitch(initialEnabled: Boolean) {
    val checked = remember { mutableStateOf(initialEnabled) }
    Switch(
        checked = checked.value,
        onCheckedChange = {
            checked.value = it
        }
    )
}

⭕️DO

@Composable
fun MySwitch(initialEnabled: Boolean) {
    var checked by remember { mutableStateOf(initialEnabled) }
    Switch(
        checked = checked,
        onCheckedChange = {
            checked = it
        }
    )
}

为什么

在 Kotlin 中,有一个称为“委托属性”的功能,它可以简单地消除每次访问值属性的需要。因此,让我们使用它来轻松编写。

4:状态更改不得在可组合函数的范围内进行。

❌DON’T

@Composable
fun MySwitch() {
    var checked by remember { mutableStateOf(false) }
    // In the scope of the Composable function, 
    // I'm changing the checked!
    checked = !checked
    Switch(
        checked = checked,
        onCheckedChange = {
            checked = it
        }
    )
}

⭕️DO

@Composable
fun MySwitch() {
    var checked by remember { mutableStateOf(false) }
    Switch(
        checked = checked,
        onCheckedChange = {
            checked = it
        }
    )
}

为什么

在此示例中,当 checked 发生更改时,MySwitch()将被无限次调用,并且开关将持续闪烁。此举是为了防止此类事故发生。
我们传递给onCheckedChange的 lambda 不是可组合函数,因此在该 lambda 中更改它是安全的。
如果您想在MySwitch()进入或退出组合时处理状态更改,则应考虑使用 SideEffect 函数。

https://developer.android.google.cn/jetpack/compose/side-effects

Compose 中的状态管理指南

与状态提升相关的实践

状态提升是一种通过将状态上移来使组件无状态的模式,当适用于可组合函数时,通常意味着引入以下两个参数

value: T // Data to display
onValueChange: (T) -> Unit // Event to request a change in the value

5:通过状态提升使可组合函数可重用和可测试

⭕️DO
Screen() 将数据流向 MySwitch(),Screen() 接收 MySwitch() 的事件。

checked: Boolean
onCheckChanged: (Boolean) -> Unit
@Composable
fun Screen() {
    var checked by remember { mutableStateOf(false) }
    MySwitch(checked = checked, onCheckChanged = { checked = it })
}

@Composable
fun MySwitch(checked: Boolean, onCheckChanged: (Boolean) -> Unit) {
    Switch(
        checked = checked,
        onCheckedChange = {
            onCheckChanged(it)
        }
    )
}

❌DON’T

@Composable
fun Screen() {
    MySwitch()
}

@Composable
fun MySwitch() {
    var checked by remember { mutableStateOf(false) }
    Switch(
        checked = checked,
        onCheckedChange = {
            checked = it
        }
    )
}

为什么

正如标题所说,使其可重用和可测试。
例如,假设您希望首先从保存的 ON 状态启动此 Switch 状态。但你不能用这个来做到这一点,所以你不能将它与其他按钮一起使用。此外,由于它只有内部状态,因此很难通过在测试中添加值来检查它。

6:使用状态提升来更改为单一可信来源。状态应至少提升到使用该状态的所有可组合项的最低公共父级。

这次,我将侧面的文字排列起来,以显示它是“ON”还是“OFF”。

⭕️DO

@Composable
fun Screen() {
    // State has only on Screen
    var checked by remember { mutableStateOf(false) }
    Row {
        MySwitch(checked = checked, onCheckChanged = { checked = it })
        Text(
            text = if (checked) "on" else "false",
            Modifier.clickable {
                checked = !checked
            }
        )
    }
}

// checked is Immutable, a value that cannot be changed.
@Composable
fun MySwitch(checked: Boolean, onCheckChanged: (Boolean) -> Unit) {
    Switch(
        checked = checked,
        onCheckedChange = {
            onCheckChanged(it)
        }
    )
}

❌DON’T

我正在使用 initialChecked 和其他方法来管理MySwitch() 中的数据。这是可重用的,也许我们可以为它编写一个测试?您认为这段代码会发生什么?

@Composable
fun Screen() {
    // I have a state in both Screen and MySwitch.
    var checked by remember { mutableStateOf(false) }
    Row {
        MySwitch(initialChecked = false, onCheckChanged = {checked = it})
        Text(
            text = if(checked) "on" else "false",
            Modifier.clickable {
                checked = !checked
            }
        )
    }
}

@Composable
fun MySwitch(initialChecked: Boolean, onCheckChanged: (Boolean) -> Unit) {
    // I have a state in both Screen and MySwitch.
    var checked by remember { mutableStateOf(initialChecked) }
    Switch(
        checked = checked,
        onCheckedChange = {
            checked = it
            onCheckChanged(it)
        }
    )

请添加图片描述

按下文本不会更新复选框并且有错误。😇

为什么要状态提升?

正如标题中提到的,这是因为可以拥有单一的可信来源。
为了防止此类错误,重要的是防止状态重复、将状态更改数量减少到单点并保持不可更改的状态流动。这是一种称为单一可信来源的结构化方法。状态提升使这成为可能。
转载请说明出处:https://blog.csdn.net/hegan2010/article/details/135266878

为什么状态应至少提升到所有使用该状态的可组合项的最低公共父级

这里的公共父级是 Screen() 的 Compose 函数,DO 示例在这里有 State。
在 DO 示例中,我们在这里有 State,因为如果它不是共同的父级,则很难使其成为单一可信来源。

7:仅将必要的参数传递给 Composable 函数

通过分离 StatelessStateful 函数,我们可以只传递我们需要的参数。

⭕️ DO

无状态视图模型有状态视图模型分开,仅将必要的数据传递给无状态可组合函数。

// stateful
@Composable
fun SettingScreen(
    settingViewModel: SettingViewModel = viewModel()
) {
    val isDarkMode by settingViewModel.isDarkMode.collectAsState()
    SettingScreen(isDarkModeSetting = isDarkMode, onDarkModeSettingChanged = {
        settingViewModel.onDarkModeChange(it)
    })
}

// stateless
@Composable
fun SettingScreen(isDarkModeSetting: Boolean, onDarkModeSettingChanged: (Boolean) -> Unit) {
    MySwitch(checked = isDarkModeSetting, onCheckChanged = {
        onDarkModeSettingChanged(it)
    })
}

❌DON’T

@Composable
fun SettingScreen(
    settingViewModel: SettingViewModel = viewModel()
) {
    val isDarkMode by settingViewModel.isDarkMode.collectAsState()
    MySwitch(checked = isDarkMode, onCheckChanged = {
        settingViewModel.onDarkModeChange(it)
    })
}

为什么

  • 无状态可组合项可以轻松预览和测试。(在这种情况下,我们只有一个按钮,所以我们可以预览 MySwitch(),但如果屏幕上有很多项目,那就会很麻烦🤔)
  • Composable 函数可以在需要复用的时候复用。

8:尽量不要将 Composition 中持有的状态传递给架构组件的 ViewModel

❌DON’T

class SettingViewModel(
    val scaffoldState: ScaffoldState
) : ViewModel()

@Composable
fun SettingScreen() {
  val scaffoldState = rememberScaffoldState()
  val settingViewModel = viewModes(factory = { SettingViewModel.Factory.create(scaffoldState) })
}

为什么

这是因为 ViewModel 和 Composition 的生命周期不同,ViewModel 的生命周期更长,所以如果ViewModel 中有 scaffoldState,ViewModel 将继续拥有旧的 scaffoldState 并发生内存泄漏。

9:尽量避免将逻辑写入可组合函数中。

在此示例中,此准则不是必需的,因为它一点也不复杂,但如果可组合项变得更复杂,则需要对其进行拆分。
假设我们更改 Snackbar 以在 check 更改时显示。
请添加图片描述

❌DON’T

还是很简单,但是各种流程都进入到 SettingScreen()的 Composable 函数中了😭

@Composable
fun SettingScreen(
    settingViewModel: SettingViewModel = viewModel(),
    scaffoldState: ScaffoldState = rememberScaffoldState(),
    coroutinesScope: CoroutineScope = rememberCoroutineScope(),
) {
    val isDarkMode by settingViewModel.isDarkMode.collectAsState()
    SettingScreen(
        scaffoldState = scaffoldState,
        isDarkModeSetting = isDarkMode,
        onDarkModeSettingChanged = {
            settingViewModel.onDarkModeChange(it)
            coroutinesScope.launch {
                scaffoldState.snackbarHostState.showSnackbar("Dark mode changed!")
            }
        }
    )
}

@Composable
fun SettingScreen(
    isDarkModeSetting: Boolean,
    onDarkModeSettingChanged: (Boolean) -> Unit,
    scaffoldState: ScaffoldState
) {
    Scaffold(scaffoldState = scaffoldState) {
        MySwitch(checked = isDarkModeSetting, onCheckChanged = {
            onDarkModeSettingChanged(it)
        })
    }
}

⭕️DO

使用 State 持有者,我能够将逻辑分开很多,并且 Composable 函数变得非常简单。

@Composable
fun SettingScreen(
    settingScreenState: SettingScreenState = rememberSettingScreenState()
) {
    SettingScreen(
        scaffoldState = settingScreenState.scaffoldState,
        isDarkModeSetting = settingScreenState.isDarkMode,
        onDarkModeSettingChanged = {
            settingScreenState.onDarkModeChange(it)
        }
    )
}

@Composable
fun SettingScreen(
    isDarkModeSetting: Boolean,
    onDarkModeSettingChanged: (Boolean) -> Unit,
    scaffoldState: ScaffoldState
) {
    Scaffold(scaffoldState = scaffoldState) {
        MySwitch(checked = isDarkModeSetting, onCheckChanged = {
            onDarkModeSettingChanged(it)
        })
    }
}

// SettingScreenState is the state holder.
// SettingScreenState is now just a class that does not inherit anything.
// This is to preserve the state of Compose so that it is not bound to any other lifecycle.
class SettingScreenState(
    // You can have other Compose States.
    val scaffoldState: ScaffoldState,
    // If you want to access business logic or screen state, 
    // the State holder can also depend on the ViewModel.
    // This is not a problem because ViewModel has a longer lifetime.
    private val settingViewModel: SettingViewModel,
    private val coroutinesScope: CoroutineScope,
) {
    // Variables, etc. can be made composable if necessary.
    val isDarkMode: Boolean
        @Composable get() = settingViewModel.isDarkMode.collectAsState().value

    fun onDarkModeChange(isDarkMode: Boolean) {
        settingViewModel.onDarkModeChange(isDarkMode)
        coroutinesScope.launch {
            scaffoldState.snackbarHostState.showSnackbar("Dark mode changed!", )
        }
    }
}

@Composable
fun rememberSettingScreenState(
    scaffoldState: ScaffoldState = rememberScaffoldState(),
    coroutinesScope: CoroutineScope = rememberCoroutineScope(),
    settingViewModel: SettingViewModel = viewModel(),
) = remember {
    SettingScreenState(
        coroutinesScope = coroutinesScope,
        scaffoldState = scaffoldState,
        settingViewModel = settingViewModel
    )
}

为什么

这是为了分离关注点。

一位著名的 Android 开发者也表示,在设计 Compose UI 时需要小心,因为 Compose 往往会将业务逻辑与 UI 混合在一起。

https://youtu.be/nVTSyLnv_4w?t=1387

Android 开发者提供了以下关于如何分离逻辑的指南。

* 用于简单 UI 元素状态管理的可组合项。
* 复杂 UI 元素状态管理的状态持有者。它们拥有 UI 元素的状态和 UI 逻辑。
* 架构组件 ViewModels 作为一种特殊类型的状态持有者,负责提供对业务逻辑和屏幕或 UI 状态的访问。

您还可以具有如下依赖项
请添加图片描述

https://developer.android.google.cn/jetpack/compose/state#managing-state

状态持有者用于管理复杂 UI 元素的状态,在这种情况下,需要它们是因为随着 Composable 的添加,Snackbar 管理的角色变得更加复杂。
正如您所看到的,UI 越复杂,您就越需要状态持有者。

总结

  • 什么是 State ?“所有的值都可能随着时间的推移而改变”。
  • 我介绍了 Jetpack Compose 中管理状态的 9 条准则。
  • 如何使用 Jetpack Compose 来管理状态。
    您需要了解 Jetpack Compose 才能正确运行它。
  • 基本思想是单一可信来源和关注点分离。即使在 Compose 之外了解它们也可能很有用。

如果您有任何反馈,请告诉我。我想修改一下这篇文章。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值