Jetpack Compose——什么时候应该使用 derivedStateOf?

derivedStateOfAPI应该在状态变化超过UI更新需求时使用,以减少不必要的重组。它类似于KotlinFlows的distinctUntilChanged,用于创建只在需要时更新的状态对象。文章通过例子解释了何时何地使用derivedStateOf,以及与remember和状态变化的关系,强调只有在输入和输出变化不同步时才需要使用。此外,还讨论了derivedStateOf在处理列表滚动、表单验证等场景的有效性,并提醒在某些情况下,如状态变化与UI更新同步时,不需要使用derivedStateOf。

derivedStateOf — 一个普偏的问题的问题是什么时候什么位置去正确的使用此API?

这个问题的答案是 derivedStateOf {} 应该在你的状态或键的变化超过你想要更新你的UI 时使用。或者换句话说,derivedStateOf 就像 Kotlin Flows或其他类似反应式框架的 distinctUntilChanged。请记住,当可重组项的状态或key发生了变化,它们会进行重组。 derivedStateOf 允许您创建一个新的状态对象,它只根据您的需要进行更改。

让我们看一个例子。这里我们有一个用户名字段和一个在用户名有效时启用的按钮。

var username by remember { mutableStateOf("") }
val submitEnabled = isUsernameValid(username)

它一开始是空的,所以我们的状态是错误的。现在,当用户开始输入时,我们的状态会正确更新并且我们的按钮会启用。 

但这就是问题所在,因为我们的用户不断输入,我们正在不必要地一遍又一遍地向我们的按钮发送状态。

这就是 derivedStateOf 的用武之地。我们的状态变化超过了我们需要更新 UI 的次数,因此 derivedStateOf 可用于此以减少重组次数。 

var username by remember { mutableStateOf("") }
val submitEnabled = remember {
  derivedStateOf { isUsernameValid(username) }
}

让我们再次通过相同的示例来查看不同之处。

用户开始输入,但这次我们的用户名状态是唯一改变的。提交状态保持为真。当然,如果我们的用户名无效。我们的派生状态再次正确更新。

现在,这个例子有点过于简单了。在真实的应用程序中,Compose 很可能会跳过提交可组合项的重组,因为它的输入参数没有改变。

现实中,你需要用到derivedStateOf的情况很少,但是当你有类似的场景时,它可以非常有效地减少重组。

始终记住,输入参数和输出结果之间的变化数量需要有所不同,才有必要使用derivedStateOf。

一些可以使用derivedStateOf的示例(不是全部):

1、观察滚动是否通过阈值(scrollPosition > 0)
2、列表中的项数大于阈值(items > 0)
3、如上所述的表单验证(username.isValid())

现在,让我们看看关于derivedStateOf的其他一些常见问题。

1、是否有必要记住derivedStateOf ?

如果它在可组合函数中,则有必要。derivedStateOf就像mutableStateOf或任何其他需要在重组中保存下来的对象一样。如果在可组合函数中使用它,则应该将其包装在remember中,否则在每次重组时它都会被重新分配值。

@Composable
fun MyComp() {
  // We need to use remember here to survive recomposition
  val state = remember { derivedStateOf { … } }
}

class MyViewModel: ViewModel() {
  // 这里我们不需要remember(也不能使用它),因为ViewModel在Composition之外。
  val state = derivedStateOf { … }
}

2、remember(key)和derivedStateOf之间有什么区别?

用每个状态作为key键的remember和derivedStateOf乍一看似乎非常相似。 

val result = remember(state1, state2) { calculation(state1, state2) }
val result = remember { derivedStateOf { calculation(state1, state2) } }

remember(key)和derivedStateOf之间的区别在于重组的数量。当你的状态或键的变化多于你想要更新UI时,使用derivedStateOf{}。

例如,仅当用户滚动了LazyColumn时才启用按钮。

val isEnabled = lazyListState.firstVisibileItemIndex > 0

firstVisibleItemIndex会随着用户滚动而改变0,1,2等,每次改变都会导致读者重新组合。我们只关心它是否大于0。我们拥有的输入量和我们需要的输出量是不同的,因此这里使用了derivedStateOf来减少不必要的重组。

val isEnabled = remember {
derivedStateOf { lazyListState.firstVisibleItemIndex > 0 }
}

现在,假设我们有一个开销比较大的函数,它接收一个参数为我们返回计算后的一个结果。我们希望UI在函数的返回发生变化时重新组合(注意,函数也是幂等的)。在这里我们使用带有key的remember,因为我们的UI需要更新的次数和键的变化一样多。也就是说,我们有相同数量的输入和输出。

val output = remember(input) { expensiveCalculation(input) }

3、我是否需要同时使用remember(key)和derivedStateOf ?什么时候需要?

这就是事情变得有点棘手的地方。derivedStateOf只能在读取到Compose state object时更新。在derivedStateOf内部读取的任何其他变量将在创建derived state时捕获该变量的初始值。如果你需要在计算中使用这些变量,你可以把它们作为remember的key。通过一个例子,这个概念更容易理解,让我们以前面的isEnabled示例为例,并将其扩展一下,使其也具有一个启用按钮的阈值,而不是0。

@Composable
fun ScrollToTopButton(lazyListState: LazyListState, threshold: Int) {
  // There is a bug here
  val isEnabled by remember {
    derivedStateOf { lazyListState.firstVisibleItemIndex > threshold }
  }
  
  Button(onClick = { }, enabled = isEnabled) {
    Text("Scroll to top")
  }
}

这里我们有一个按钮,当列表滚动超过阈值时按钮可点。我们正确地使用了derivedStateOf来删除额外的重组,但是有一个错误。如果threshold参数改变,我们的derivedStateOf将忽略threshold参数的改变,因为derivedStateOf在创建时为所有非compose state object的变量赋予初始值。由于threshold是Int类型,因此将捕获传递给可组合对象的第一个值,并将其用于此后的计算。ScrollToTopButton仍然会重新组合,因为它的输入已经改变了,但是请记住,在重组过程中没有任何键缓存,它不会用新值重新初始化derivedStateOf。

我们可以通过查看代码的输出来了解这一点。一开始一切都很正常。

但是,一个新的的阈值(5)被传递到我们的组合时。 

即使我们的scrollPosition小于threshold, isEnabled仍然被设置为true。

这里的修复是将threshold添加为一个key,记住,这将在threshold发生变化时重新初始化我们的derivedStateOf状态。 

val isEnabled by remember(threshold) {
  derivedStateOf { lazyListState.firstVisibleItemIndex > threshold }
}

现在我们可以看到,当阈值改变时,isEnabled状态正确更新。

 4、我是否需要使用derivedStateOf来将多个状态组合在一起?

 大多数情况是不需要的。如果您有多个状态组合在一起以创建一个结果,那么您可能希望只要其中一个状态发生变化就可以进行重组。

以一个输入名字和姓氏并显示全名的表单为例。

var firstName by remember { mutableStateOf("") }
var lastName by remember { mutableStateOf("") }

// This derivedStateOf is redundant
val fullName = remember { derivedStateOf { "$firstName $lastName" } }

在这里,由于输出的变化和输入的变化一样多,所以derivedStateOf没有做任何事情,只是造成了很小的开销。derivedStateOf对异步更新也没有帮助,Compose状态快照系统是单独处理的,这里的调用是同步的。 在这种情况下,根本不需要额外的派生状态对象。

var firstName by remember { mutableStateOf("") }
var lastName by remember { mutableStateOf("") }

val fullName = "$firstName $lastName"

总结:

总而言之,请记住,当您的状态或键的更改多于您希望更新的UI时,将使用derivedStateOf。如果输入量和输出量没有差别,就不需要使用它。

Jetpack Compose 是一个现代化的 Android UI 工具包,它采用声明式编程模型,并与 Kotlin 语言紧密结合,以简化和加速 UI 开发流程。然而,Jetpack Compose 并不支持使用 Java 语言来编写 UI 组件,这与传统的 XML 布局方式不同。 尽管 Java 仍然是 Android 开发中广泛使用语言之一,但 Jetpack Compose 从设计之初就专注于 Kotlin,这是由于 Kotlin 提供了更适合声明式 UI 编程的语言特性,例如函数式组件、高阶函数和类型推断等。因此,Jetpack Compose 的 API 和开发工具都优先支持 Kotlin,而 Java 并不在官方支持的范围内。这意味着开发者无法使用 Java 来编写 Jetpack Compose 的 UI 代码,也无法通过 Java 来替代 XML 布局文件。 对于那些希望采用 Jetpack Compose 的项目来说,这意味着需要从 XML 布局转向使用 Kotlin 编写的 Composable 函数来定义 UI。尽管如此,Jetpack Compose 并没有完全抛弃传统的 XML 布局系统,而是允许开发者在 Compose 中嵌入传统的 `View` 系统组件,从而实现新旧代码的共存。这种互操作性为逐步迁移到 Jetpack Compose 提供了可能,尤其是在大型的遗留项目中。 尽管如此,对于新项目或者可以完全重构的项目来说,使用 Jetpack Compose 可以带来更简洁的代码结构、更快的开发速度以及更好的可维护性。Jetpack Compose 的声明式编程模型使得 UI 逻辑更加直观,并且减少了状态同步的复杂度。此外,Jetpack Compose 的预览功能也极大地提高了开发效率,允许开发者在不运行应用的情况下查看和调试 UI 组件。 然而,对于仍然主要使用 Java 的项目来说,转向 Jetpack Compose 可能会面临一定的挑战。这是因为 Jetpack Compose 的核心理念和实现方式都基于 Kotlin,而 Java 开发者可能需要学习 Kotlin 才能充分利用 Jetpack Compose 的优势。此外,Jetpack Compose 的某些高级特性可能在 Java 中无法直接使用,或者需要额外的工作来适配。 综上所述,Jetpack Compose 不支持使用 Java 编写 UI 代码,也无法通过 Java 来替代 XML 布局文件。Jetpack Compose 的设计初衷是为了提供一种更加现代化、声明式的 UI 开发体验,而这主要依赖于 Kotlin 语言特性的支持。尽管如此,Jetpack Compose 仍然允许与传统的 XML 布局和 Java 代码共存,以便于现有项目的逐步迁移。 ```kotlin @Composable fun Greeting(name: String) { Text(text = "Hello $name!") } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值