Compose的State(九)

一、前言

关于状态的解释,本篇大部分引用自Google的解释。

对状态变化做出反应是 Compose 的核心。Compose 应用程序通过调用 Composable 函数将数据转换为 UI。如果您的数据发生变化,您可以使用新数据调用这些函数,从而创建更新的 UI。Compose 提供了用于观察应用程序数据变化的工具,这些工具会自动调用您的函数——这称为重构。Compose 还会查看单个可组合组件需要哪些数据,以便它只需要重新组合数据已更改的组件,并且可以跳过组合未受影响的组件。

在底层,Compose 使用自定义 Kotlin 编译器插件,因此当底层数据发生变化时,可以重新调用可组合函数以更新 UI 层次结构。

例如,当你调用Greeting("Android")MyScreenContent可组合的功能,你是硬编码的输入("Android"),所以Greeting将添加到UI树一次,永远不会改变,即使身体MyScreenContent被重构。

要将内部状态添加到可组合,请使用该mutableStateOf函数,它提供可组合的可变内存。为了不让每次重组都有不同的状态,请记住使用remember. 而且,如果在屏幕上的不同位置有多个可组合实例,每个副本将获得自己的状态版本。您可以将内部状态视为类中的私有变量。

可组合函数将自动订阅它。如果状态发生变化,读取这些字段的可组合项将被重新组合。

二、示例

假设我们想要实现一个点击按钮增加数字的功能,我们会怎样做呢,是否如下所示:

    @Composable
    fun counter() {
        var count = 0
        Button(onClick = { count++ }) {
            Text("I've been clicked ${count} times")
        }
    }

运行上述代码会发现,其实根本就没有效果,这个现象是不是跟前文的延迟列表LazyColumn更改数据源的问题有点类似。所以这时候需要用到状态了。官方对此问题的解释如下

  • 要点:Compose 将通过读取 State 对象自动重组界面。

    如果您在 Compose 中使用 LiveData 等其他可观察类型,应该先将其转换为 State<T>,然后再使用诸如 LiveData<T>.observeAsState() 之类的可组合扩展函数在可组合项中读取它

  • 注意:在 Compose 中将可变对象(如 ArrayList<T>mutableListOf())用作状态会导致用户在您的应用中看到不正确或陈旧的数据。

    不可观察的可变对象(如 ArrayList<T> 或可变数据类)不能由 Compose 观察,因而 Compose 不能在它们发生变化时触发重组。我们建议您使用可观察的数据存储器(如 State<List<T>>)和不可变的 listOf(),而不是使用不可观察的可变对象。

本解释参考以下链接:

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

据此修改后的代码如下

    @Composable
    fun counter() {
        val count = remember { mutableStateOf(0) }
        Button(onClick = { count.value++ }) {
            Text("I've been clicked ${count.value} times")
        }
    }

三、有状态组合和无状态组合

Compose中组合分为两种,有状态组合和无状态组合

一个UI组合中包含了状态就是有状态组合,如果不包含状态就是无状态组合,例如

  • 无状态组合

    @Composable
    private fun body(){
        Text("===")
    }
    
  • 有状态组合

    @Composable
    fun counter() {
        var count by remember { mutableStateOf(0) }
        Button(onClick = { count++ }) {
            Text("I've been clicked $count times")
        }
    }
    

四、状态提升

在可组合函数中,应该公开对调用函数有用的状态,因为这是可以使用或控制它的唯一方式——这个过程称为状态提升

状态提升是使内部状态可由调用它的函数控制的方法。您可以通过受控制的可组合函数的参数公开状态并从控制可组合函数的外部实例化它来实现此目的。使状态可提升可避免重复状态和引入错误,有助于重用可组合物,并使可组合物更易于测试。可组合调用者不感兴趣的状态应该是内部状态。

在某些情况下,消费者可能不关心某个状态(例如,在滚动条中,scrollerPosition状态是公开的,而maxPosition不是)。真相的来源属于创造和控制那个状态的人。

在示例中,由于 的消费者Counter可能对状态感兴趣,因此您可以通过引入一(count, updateCount)对作为 的参数将其完全推迟给调用者Counter。这样,Counter就是吊装它的状态:

@Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
    val counterState = remember { mutableStateOf(0) }

    Column {
        for (name in names) {
            Greeting(name = name)
            Divider(color = Color.Black)
        }
        Divider(color = Color.Transparent, thickness = 32.dp)
        Counter(
            count = counterState.value,
            updateCount = { newCount ->
                counterState.value = newCount
            }
        )
    }
}

@Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
    Button(onClick = { updateCount(count+1) }) {
        Text("I've been clicked $count times")
    }
}

五、保存状态的几种方式

通过上文我们知道通常状态是使用State<T>来保存的(通常我们常常使用其具体实现,如mutableStateOf)。在我们具体开发时通常有以下几种方式声明mutableStateOf:

  • val mutableState = remember { mutableStateOf(default) }
  • var value by remember { mutableStateOf(default) }
  • val (value, setValue) = remember { mutableStateOf(default) }

但是要注意的时委托用法by需要倒入适量相关的类

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

六、ViewModel与State

出于可维护的考虑,最好将状态与UI进行分离。因此可以考虑使用ViewModel来进行State管理(以下为代码示例,不可以直接使用)

@Composable
private fun TodoActivityScreen(todoViewModel: TodoViewModel) {
   TodoScreen(
       items = todoViewModel.todoItems,
       onAddItem = todoViewModel::addItem,
       onRemoveItem = todoViewModel::removeItem
   )
}

定义的ViewModel如下

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel

class TodoViewModel : ViewModel() {

    private var currentEditPosition by mutableStateOf(-1)

    var todoItems = mutableStateListOf<TodoItem>()
        private set

    val currentEditItem: TodoItem?
        get() = todoItems.getOrNull(currentEditPosition)

    fun addItem(item: TodoItem) {
        todoItems.add(item)
    }

    fun removeItem(item: TodoItem) {
        todoItems.remove(item)
        onEditDone() // don't keep the editor open when removing items
    }

    fun onEditItemSelected(item: TodoItem) {
        currentEditPosition = todoItems.indexOf(item)
    }

    fun onEditDone() {
        currentEditPosition = -1
    }

    fun onEditItemChange(item: TodoItem) {
        val currentItem = requireNotNull(currentEditItem)
        require(currentItem.id == item.id) {
            "You can only change an item with the same id as currentEditItem"
        }

        todoItems[currentEditPosition] = item
    }
}

七、LiveData转换为State

Jetpack Compose 并不要求您使用 MutableState<T> 存储状态。Jetpack Compose 支持其他可观察类型。在 Jetpack Compose 中读取其他可观察类型之前,您必须将其转换为 State<T>,以便 Jetpack Compose 可以在状态发生变化时自动重组界面。

Compose 附带一些可以根据 Android 应用中使用的常见可观察类型创建 State<T> 的函数:

但是由于我只对LiveData熟悉,因此这里只记录LiveData的用法

class HelloViewModel : ViewModel() {

    // LiveData holds state which is observed by the UI
    // (state flows down from ViewModel)
    private val _name = MutableLiveData("")
    val name: LiveData<String> = _name

    // onNameChange is an event we're defining that the UI can invoke
    // (events flow up from UI)
    fun onNameChange(newName: String) {
        _name.value = newName
    }
}

@Composable
fun HelloScreen(helloViewModel: HelloViewModel = viewModel()) {
    // by default, viewModel() follows the Lifecycle as the Activity or Fragment
    // that calls HelloScreen(). This lifecycle can be modified by callers of HelloScreen.

    // name is the current value of [helloViewModel.name]
    // with an initial value of ""
    val name: String by helloViewModel.name.observeAsState("")
    HelloContent(name = name, onNameChange = { helloViewModel.onNameChange(it) })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

其主要是使用以下的方式进行状态存储的

val name: String by helloViewModel.name.observeAsState("")

上述代码等效于下文

val nameState: State<String> = helloViewModel.name.observeAsState("")

八、状态保存

在重新创建 activity 或进程后,您可以使用 rememberSaveable 恢复界面状态。rememberSaveable 可以在重组后保持状态。此外,rememberSaveable 也可以在重新创建 activity 和进程后保持状态。

在这里有以下几种方式:

存储状态的方式

添加到 Bundle 的所有数据类型都会自动保存。如果要保存无法添加到 Bundle 的内容,您有以下几种选择。

Parcelize

最简单的解决方案是向对象添加 @Parcelize 注解。对象将变为可打包状态并且可以捆绑。例如,以下代码会创建可打包的 City 数据类型并将其保存到状态。

@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable {
        mutableStateOf(City("Madrid", "Spain"))
    }
}
MapSaver

如果某种原因导致 @Parcelize 不合适,您可以使用 mapSaver 定义自己的规则,规定如何将对象转换为系统可保存到 Bundle 的一组值。

data class City(val name: String, val country: String)

val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },//这里面的内容由restore里面的对象进行确定
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}
ListSaver

为了避免需要为映射定义键,您也可以使用 listSaver 并将其索引用作键:

data class City(val name: String, val country: String)

val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

九、remember

为了避免每次重组时候都生成新的状态,可以使用remember对状态进行缓存。remember的使用方式如下

remember() { mutableStateOf(default) }

另外这个还可以通过key来判断是否根据key来生成新值,如果没有key的话,则始终返回重组时候生成的新值

remember(todo.id) { randomTint() }

记住调用有两个部分:

  1. 关键参数——这个记忆使用的“关键”,这是在括号中传递的部分。在这里,我们todo.id作为关键传递。
  2. 计算– 一个计算要记住的新值的 lambda,在尾随 lambda 中传递。这里我们用 计算一个随机值randomTint()

十、状态改变导致UI重组

在Compose的设计中,整个UI树会随着状态的改变而进行UI的重绘制。根据地层算法的判断,每次会重绘发生改变的部分,这样就使得UI能够及时响应。

下面示例实现了一个点击按钮使数量增加的功能

    @Composable
    fun counter() {
        val count = mutableStateOf(0)
        Button(onClick = { count.value++ }) {
            Log.e("Ym","重组Text数据")
            Text("I've been clicked ${count.value} times")
        }
        Log.e("Ym","重组数据")
    }

通过日志可以发现,整个counter()函数只执行了一次,而Button里面的函数,每次点击都会执行,然后Text重新绘制,页面上的数量也跟着增加了。

如果我们把Button里面日志中的${count.value}去掉,则会发现不管点击几次,Button里面的函数都不会执行。

@Composable
fun counter() {
    val count = mutableStateOf(0)
    Button(onClick = { count.value++ }) {
        Log.e("Ym","重组Text数据")
        Text("I've been clicked times")
    }
    Log.e("Ym","重组数据")
}

如果我们在TextLog.e("Ym","重组数据")中都加上${count.value}变成以下方式,则发现,每次点击时候使数量增加时候,不管是Button还是counter()函数都重新执行了

@Composable
fun counter() {
    val count = mutableStateOf(0)
    Button(onClick = { count.value++ }) {
        Log.e("Ym","重组Text数据")
        Text("I've been clicked ${count.value} times")
    }
    Log.e("Ym","重组数据${count.value}")
}

因此可以证明官方所说的对State<T>数据的监听会导致页面重绘制。

然而上文在运行后发现不管点击几次数量都是0,这是因为每次重绘制都会导致 val count = mutableStateOf(0)重新执行,然后数量就归0了。这时候就可以使用remember来对状态进行缓存。或者将该组合变成无状态组合,将状态进行提升至更高的组合中去。

var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
    Log.e("Ym","重组Text数据")
    Text("I've been clicked $count times")
}
Log.e("Ym","重组数据 $count")

十一、重组的最小范围

Compose中重组的范围主要受两种因素影响

  1. 只有会受到 state 变化影响的代码块才会参与到重组,不依赖 state 的代码不参与重组。

  2. 只有 非inline函数 才有资格成为重组的最小范围

    这是因为Compose 在编译期分析出会受到某 state 变化影响的代码块,并记录其引用,当此 state 变化时,会根据引用找到这些代码块并标记为 Invalid 。在下一渲染帧到来之前 Compose 会触发 recomposition,并在重组过程中执行 invalid 代码块。

    Invalid 代码块即编译器找出的下次重组范围。能够被标记为 Invalid 的代码必须是非 inline 且无返回值@Composalbe function/lambda,必须遵循 重组范围最小化 原则。

    为什么inline函数无法编译,是因为kotlin编译后,被inline标记的函数会移动到调用的函数里面,所以编译后的源码是没有inline函数的。

    而其中ColumnRowBox 乃至 Layout 这种容器类 Composable 都是 inline 函数。针对这个解释可以参考下面的示例

    @Composable
    fun Foo() {
    
        var text by remember { mutableStateOf("") }
    
        Button(onClick = { text = "$text $text" }) {
            Log.d(TAG, "Button content lambda")
            Box {
                Log.d(TAG, "Box")
                Text(text).also { Log.d(TAG, "Text") }
            }
        }
    }
    

十二、缩小重组范围

根据上文知道最小重组最小范围是非inline函数。那么如果想使其被inline标记的组合缩小重组范围如何做呢,可以使用Wrapper来进行处理,如下

@Composable
fun Foo() {

    var text by remember { mutableStateOf("") }

    Button(onClick = { text = "$text $text" }) {
        Log.d(TAG, "Button content lambda")
        Wrapper {
            Text(text).also { Log.d(TAG, "Text") }
        }
    }
}

@Composable
fun Wrapper(content: @Composable () -> Unit) {
    Log.d(TAG, "Wrapper recomposing")
    Box {
        Log.d(TAG, "Box")
        content()
    }
}

十三、@Stable & @Immutable

页面的重组会刷新UI,但是频繁多余的重组会带来性能问题,避免多余的重组可以减少意外情况。在Compose中将数据类型视为稳定的和不稳定的,稳定的数据类型会根据情况来判断是否需要重组,但是不稳定的数据类型一定会认为需要重组。关于稳定性具体可以参考下文

Compose 类型稳定性注解:@Stable & @Immutable
Compose 中的稳定性

十四、参考链接

  1. Compose基础-状态

https://developer.android.google.cn/codelabs/jetpack-compose-basics#4

  1. 管理状态

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

  1. Parcelize

https://github.com/Kotlin/KEEP/blob/master/proposals/extensions/android-parcelable.md

  1. Using state in Jetpack Compose

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

  1. remember

https://developer.android.google.cn/reference/kotlin/androidx/compose/runtime/package-summary#remember(kotlin.Function0)

  1. 了解Compose的重组作用域

    https://compose.net.cn/principle/recomposition_scope/

  2. Jetpack Compose学习(8)——State及remeber

  3. 遵循最佳做法

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值