Compose 状态管理
什么是状态
Compose是声明式UI,UI的变化都是由数据的状态驱动的,在了解状态管理之前,先要了解为什么需要状态管理。
Compose界面的构成都是由一个个可组合函数构成的,当可组合函数的输入发生变化时,系统会重新执行这个可组合函数并重新构建UI,例如TextView显示文本,当文本内容发生变化时,随之UI也需要发生变化,下面是状态驱动UI变化的实现:
kt
setContent {
var text = "Hello World"
StateTest(text) {
text = "Hello Compose"
}
}
@Composable
fun StateTest(text: String, onClick: () -> Unit) {
Text(
text = text,
color = Color.Black,
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
.clickable {
onClick()
}
)
}
以上代码想要的结果是当点击Text时,Text显示的文本发生改变,但是当我们点击时,发现文本内容并没有改变,此时就需要通过状态来控制Compose函数重组时的UI变化。此时就需要用到remember
和SnapshotState
,remember
是保存一个表达式计算的值,这个值发生变化会导致使用这个值作为输入参数的Compose函数发生重组,snapshotState 是 Compose 状态管理系统的核心机制,它通过快照系统,允许Compose函数根据输入进行自动重组,不再是传统View中需要手动更新。将上面的代码实现为状态管理的方式更新UI,代码如下:
kt
setContent {
val text = "Hello World"
var rememberText by remember {
mutableStateOf(text)
}
StateTest(rememberText) {
rememberText = "Hello Compose"
}
}
@Composable
fun StateTest(text: String, onClick: () -> Unit) {
Text(
text = text,
color = Color.Black,
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
.clickable {
onClick()
}
)
}
此时,Text点击事件中,我们改变了rememberText的值,因为这个值是被remember记录的,会触发StateTest函数的重组,UI界面发生变化。
remember函数提供重载的函数有一个至多个参数,
kt
@Composable
inline fun <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T =
currentComposer.cache(false, calculation)
@Composable
inline fun <T> remember(
key1: Any?,
crossinline calculation: @DisallowComposableCalls () -> T
): T {
return currentComposer.cache(currentComposer.changed(key1), calculation)
}
可以根据自己的需求传入不同的计算条件参与值的计算,从而根据缓存的值判断Compose函数在数据源发生变化时是否需要重组:
例如我们在Compose函数首次重组时,根据传入的List数据源计算值大于60的数据集合,并在列表项里将大于60的item背景色标记为红色,可以实现如下:
kt
@Composable
fun ScoreFlagList(scoreList: MutableList<Int>) {
val passScoreList = remember(scoreList) {
scoreList.filter { it > 60 }.map { scoreList.indexOf(it) }.toSet()
}
Column {
scoreList.forEach {
Text(
text = "score: $it",
modifier = Modifier
.fillMaxWidth()
.height(40.dp)
.background(
color = if (passScoreList.contains(scoreList.indexOf(it))) {
Color.Green
} else {
Color.Red
}
)
)
}
}
}
状态管理相关API与用法
Compose函数因为数据变化而发生重组必须是State类型的数据,并且被remember缓存在Compose函数的内存快照中,对于一些可变但不可被观察的数据,例如ArrayList、mutableListOf或可变数据类等,我们必须使用例如MutableState<ArrayList>的形式,让数据能被可组合函数观察到。同时Compose并不是强制要求MutableState存储状态,也可以将其他可观察的数据转为Compose可观察的状态,例如LiveData,Flow等
- 将可变List集合转换为可观察对象
kt
val list = mutableListOf(65, 76, 98, 86, 11, 74, 68, 72, 97, 91)
val stateList = remember { mutableStateOf(list) }
- 将LiveData转为状态变量
kt
@Composable
fun LiveDataState(textString: MutableLiveData<Int>) {
val liveData = textString.observeAsState()
Text(
text = "Number = ${liveData.value}", modifier = Modifier
.fillMaxWidth()
.height(30.dp)
.clickable {
textString.value = textString.value!! + 1
})
}
- 将Flow数据转换为State变量
kt
@Composable
fun FlowDataState(textString: MutableStateFlow<Int>) {
val liveData = textString.collectAsState(initial = 0)
Text(
text = "Number = ${liveData.value}", modifier = Modifier
.fillMaxWidth()
.height(30.dp)
.clickable {
textString.update { it + 1 }
})
}
官方还提供了RxJava的数据变化时,转换为State的相关API,同时自定义的一些可观察的数据应该使用produceState
API来产生状态。
单向数据流
在传统的View体系中,假设我们需要实现一个Text用于实时显示输入框输入的内容,我们可以这样实现:
kt
class MainActivity2 : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.editText.doAfterTextChanged { text ->
binding.helloText.text = "hello, ${text.toString()}"
}
}
}
当输入框数据发生变化时,会回调doAfterTextChanged事件,然后将数据更新到TextView中,在复杂的场景中,一个事件导致一个UI的更新,同时也可能导致另一个UI的更新或者数据变化,这种状态变化称之为非结构化状态,在Android传统开发中,这种非结构化状态无法进行系统性的单元测试,View与事件处理与数据是混合在一起的。因此引入ViewModel和LiveData进行事件、数据和UI的分离。
kt
class MainActivity2 : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val inputTextViewModel by viewModels<InputTextViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.editText.doAfterTextChanged { text ->
inputTextViewModel.onInputTextChange(text.toString())
}
inputTextViewModel.inputText.observe(this) {
binding.helloText.text = it
}
}
}
class InputTextViewModel : ViewModel() {
private val _inputText = MutableLiveData<String>()
val inputText: LiveData<String> = _inputText
fun onInputTextChange(text: String) {
_inputText.value = text
}
}
这样UI的事件流向ViewModel,ViewModel中改变数据,UI监听LiveData,LiveData数据被改变驱动UI更新,这种方式称之为单向数据流。
单向数据流的优点如下:
- 可预测性与调试简化
- 状态隔离与安全性
- 可扩展性提升
状态提升
前面介绍了状态,状态更新一般由事件产生,常见的用户的输入事件,例如点击、长按,或者一些其他事件导致数据变化的事件,此时状态就应该更新,随之更新UI。而Compose最大的特点就是可组合的,我们写的空间要想拥有很强的复用性,我们就需要状态提升,状态提升就是把状态作为参数传入给可组合函数,状态的变化由调用方控制。
kt
class ComposeLearnActivity : ComponentActivity() {
private val inputTextViewModel by viewModels<InputTextViewModel2>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
InputTextPreviewComponent(inputText = inputTextViewModel.inputText, onInputTextChanged = {
inputTextViewModel.onInputTextChange(it)
})
}
}
}
@Composable
fun InputTextPreviewComponent(inputText: State<String>, onInputTextChanged: (String) -> Unit) {
Column {
Text(text = inputText.value)
TextField(value = inputText.value, onValueChange = onInputTextChanged)
}
}
class InputTextViewModel2 : ViewModel() {
private val _inputText: MutableState<String> = mutableStateOf("")
val inputText: State<String> = _inputText
fun onInputTextChange(text: String) {
_inputText.value = text
}
}
以上代码就是一个状态提升的实例,我们可以看到InputTextPreviewComponent函数内部只处理UI的显示和事件的回调,并没有操作ViewModel的数据,因此复用起来更方便。
状态保存与恢复
Activity在某些情况下可能会销毁重建,例如屏幕旋转没有正确配置configChanges
设置,如果Activity销毁重建了,我们还需要保留之前的状态,就需要使用到rememberSaveable
将状态保存到Bundle中,在重建的时候读取数据并恢复状态。先看一个不保存状态的例子:
kt
class ComposeLearnActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("Compose", "onCreate")
setContent {
val countNUm = remember { mutableStateOf(0) }
TextCounter(countNUm, {
countNUm.value++
})
}
}
override fun onDestroy() {
super.onDestroy()
Log.d("Compose", "onDestroy")
}
}
@Composable
fun TextCounter(countNUm: State<Int>, clickable: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
"当前计数:${countNUm.value}",
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 10.dp)
.background(Color(0xFFE0E0E0))
.clickable { clickable() },
textAlign = TextAlign.Center,
fontSize = 15.sp,
color = Color(0xFF000000),
)
}
}
Activity重建之后还会执行生命周期方法onCreate
此时TextCounter
会重组,此时的值还是默认值0。此时只需要将remember
改为rememberSaveable
,销毁重建之后还是原来的值。
kt
val countNUm = rememberSaveable{
mutableStateOf(0)
}
rememberSaveable
会自动将基础类型的数据存储到Bundle
中,如果你需要保存的数据是自定义类型的,比如数据类,你可以使用不同的存储机制,例如使用 Parcelize
注解、使用 listSaver
和mapSaver
等 Compose API,或实现会扩展 Compose 运行时 Saver
类的自定义 Saver
类。
例如使用Parcelize注解:@kotlinx.parcelize.Parcelize
是一个gradle插件,需要导入相关插件
kt
@kotlinx.parcelize.Parcelize
data class City(var name: String, var score: Int) : Parcelable
在onCreate方法中,我们只需要使用rememberSaveable进行状态保存与恢复即可实现
kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("Compose", "onCreate")
setContent {
val city = rememberSaveable {
mutableStateOf(City("上海", 82))
}
CityShow(city) {
city.value = city.value.copy(
score = city.value.score + 1
)
}
}
}
还有一种方式就是使用Compose提供的Saver
,可以看到官方已经实现了一些保存容器状态的Saver
,我们要实现自己的自定义Saver
可以参考官方的实现。官方文档地址
kt
fun <Original, Saveable> listSaver(
save: SaverScope.(value: Original) -> List<Saveable>,
restore: (list: List<Saveable>) -> Original?
): Saver<Original, Any> =
@Suppress("UNCHECKED_CAST")
Saver(
save = {
val list = save(it)
for (index in list.indices) {
val item = list[index]
if (item != null) {
require(canBeSaved(item)) { "item can't be saved" }
}
}
if (list.isNotEmpty()) ArrayList(list) else null
},
restore = restore as (Any) -> Original?
)