Jetpack Compose State

更新日期: 2021-09-12 阅读次数: 3210 字数: 1341 分类: Android

目标

目标是写一个基于 jetpack compose 的表单页面,用于将一系列产品数据存储于本地 sqlite 数据库中。 但是真正动手时,发现不知道如何获取 input 组件的输入值,这不再是 findbyid 那种获取输入值的形式。 所以,不得不了解一下 compose state 的机制。

还有另外一个疑问。对于 data class 这样的多字段结构化数据,state 机制如何方便地映射到每一个输入组件上。

什么是 state

State in an application is any value that can change over time.

For example it may be a value stored in a Room database, a variable on a class, or even the current value read from an accelerometer.

compose state 从代码上看非常像 reactjs 的 useState hook

state 是如何变化的

In Android apps, state is updated in response to events.  Events are inputs generated from outside our application, such as the user tapping on a button calling an OnClickListener, an EditText calling afterTextChanged, or an accelerometer sending a new value.

名词

  • unidirectional 单向: unidirectional data flow.
  • state hoisting: 状态提升
  • recomposition: 改写:即,用新的 value 参数,重新调用 composable

state 在 ViewModel 中的形式

In a ViewModel, state is represented by LiveData. A LiveData is an observable state holder, which means that it provides a way for anyone to observe changes to the state. Then in the UI we use the observe method to update the UI whenever the state changes.

整个 flow 可以理解成:

  • 用户输入事件触发 ViewModel 中的函数处理 someStateChanged
  • someStateChanged 修改 ViewModel 中的 state,即 LiveData
  • LiveData 的监听者,通知 UI,"state 发生了变化"

这个模式称之为单向数据流,即 unidirectional data flow。

注:实际使用中,LiveData 可以简化为 mutableStateListOf.

State hoisting

即,状态提升。目的是实现 stateless 的 composable。

A stateless composable is a composable that cannot directly change any state.

好处是,方便自动化测试,组件复用。

以 todo 列表为例,stateless composable 的实现逻辑是:

  • 展示传入的 todo items
  • 删除、添加 todo item 由传入的两个函数处理

简单抽象为,向 stateless composable 传递两个参数:

  • value: T, 即待展示的数据。例如,ViewModel 中定义的 LiveData 类型的 items.
  • onValueChange: (T) -> Unit,T 为最新的值,这个函数用于向 ViewModel 更新 state。例如,ViewModel 中定义的 addItem.

状态提示带来的结果,我们可以将 remembered state 从 composable 中去除。

because the state is now hoisted, we remove the remembered state from TodoInputTextField.

详细参考:

https://developer.android.com/codelabs/jetpack-compose-state#3

mutableStateListOf

ViewModel 中可以使用 mutableStateListOf 创建一个可监听的 mutable list。

mutableStateListOf allows us to create an instance of MutableList that is observable.

mutableStateListOf 是专为 compose 设计,如果还需要兼容 xml view,需要改用 LiveData。

看看 Kotlin 奇葩的语法糖:

var todoItems = mutableStateListOf<TodoItem>()
	private set

private set 跟在变量前加 private 的区别是,private set 允许外部 get,只有 set 限制内部使用。

jetpack compose state for class

但是,现实场景中,要复杂的多。以表单录入界面为例,通常是一个对象,对应 N 个字段需要录入。

最直观的想法是,UI 中定义 N 个 state 变量来保存输入的数据,然后在点击提交按钮时调用 ViewModel 中的 add 方法。

但,如果字段繁多,就需要定义一堆 state 变量,我觉得一定有更优雅的处理方式。

在网上找到了解决方案:

// the class to be modified
data class MyThing(var name: String = "Ma", var age: Int = 0)

@Composable
fun MyScreen() {
  val (myThing, myThingSetter) = remember { mutableStateOf(MyThing()) }

  Column {
    Text(text = myThing.name)
    // button to add "a" to the end of the name
    Button(onClick = { myThingSetter(myThing.copy(name = myThing.name + "a")) }) {
      Text(text = "Add an 'a'")
    }
    // button to increment the new "age" field by 1
    Button(onClick = { myThingSetter(myThing.copy(age = myThing.age + 1)) }) {
      Text(text = "Increment age")
    }
  }
}

而官方的 todo demo 中则是拆成了两个独立的 state

@Composable
fun TodoItemEntryInput(onItemComplete: (TodoItem) -> Unit, buttonText: String = "Add") {
    val (text, onTextChange) = rememberSaveable { mutableStateOf("") }
    val (icon, onIconChange) = remember { mutableStateOf(TodoIcon.Default) }

    val submit = {
        if (text.isNotBlank()) {
            onItemComplete(TodoItem(text, icon))
            onTextChange("")
            onIconChange(TodoIcon.Default)
        }
    }

    TodoItemInput(
        text = text,
        onTextChange = onTextChange,
        icon = icon,
        onIconChange = onIconChange,
        submit = submit,
        iconsVisible = text.isNotBlank()
    ) {
        TodoEditButton(onClick = submit, text = buttonText, enabled = text.isNotBlank())
    }
}

remember 与 rememberSaveable 的区别

参考: What is the difference between "remember" and "mutableState" in android jetpack compose?

remember 是在 composable 内缓存数据,单纯的 remember 对应的变量是不可变的。

val state: Int = remember { 1 }

MutableState 变量在发生变更时,会触发 composable 的 recomposition,即重新渲染。

val state: MutableState<Int> = remember { mutableStateOf(1) }

rememberSaveable 是加强版的 remember,可以在屏幕发生旋转,即 activity 重建时保存状态。

val state: MutableState<Int> = rememberSaveable { mutableStateOf(1) }

虽然 rememberSaveable 能持久保存状态,但是很多场景下并不需要,也需要状态的重置。 只有输入内容保存能提高用户体验时,才需要,例如用户的评论输入等。

感慨

看 Using state in Jetpack Compose 教程有感:

反复重复知识点,对我很有效。而且我也并不觉得啰嗦。反而方便理解和记忆。

再就是,Compose 用起来并没有第一眼看那么简单,甚至可以说要比之前的 XML 更复杂。学习成本极高。

参考

https://developer.android.com/codelabs/jetpack-compose-state

tags: Jetpack Compose

关于作者 🌱

我是来自山东烟台的一名开发者,有敢兴趣的话题,或者软件开发需求,欢迎加微信 zhongwei 聊聊, 查看更多联系方式