Android Jetpack Compose状态管理与状态提升
Compose 状态管理
什么是状态
Compose是声明式UI,UI的变化都是由数据的状态驱动的,在了解状态管理之前,先要了解为什么需要状态管理。
Compose界面的构成都是由一个个可组合函数构成的,当可组合函数的输入发生变化时,系统会重新执行这个可组合函数并重新构建UI,例如TextView显示文本,当文本内容发生变化时,随之UI也需要发生变化,下面是状态驱动UI变化的实现:
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,代码如下:
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函数提供重载的函数有一个至多个参数,
@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背景色标记为红色,可以实现如下:
@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集合转换为可观察对象
val list = mutableListOf(65, 76, 98, 86, 11, 74, 68, 72, 97, 91)
val stateList = remember { mutableStateOf(list) }
- 将LiveData转为状态变量
@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变量
@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用于实时显示输入框输入的内容,我们可以这样实现:
class MainActivity2 : AppCompatActivity() {private lateinit var binding: ActivityMainBindingoverride 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的分离。
class MainActivity2 : AppCompatActivity() {private lateinit var binding: ActivityMainBindingprivate 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> = _inputTextfun onInputTextChange(text: String) {_inputText.value = text}
}
这样UI的事件流向ViewModel,ViewModel中改变数据,UI监听LiveData,LiveData数据被改变驱动UI更新,这种方式称之为单向数据流。
单向数据流的优点如下:
- 可预测性与调试简化
- 状态隔离与安全性
- 可扩展性提升
状态提升
前面介绍了状态,状态更新一般由事件产生,常见的用户的输入事件,例如点击、长按,或者一些其他事件导致数据变化的事件,此时状态就应该更新,随之更新UI。而Compose最大的特点就是可组合的,我们写的空间要想拥有很强的复用性,我们就需要状态提升,状态提升就是把状态作为参数传入给可组合函数,状态的变化由调用方控制。
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> = _inputTextfun onInputTextChange(text: String) {_inputText.value = text}
}
以上代码就是一个状态提升的实例,我们可以看到InputTextPreviewComponent函数内部只处理UI的显示和事件的回调,并没有操作ViewModel的数据,因此复用起来更方便。
状态保存与恢复
Activity在某些情况下可能会销毁重建,例如屏幕旋转没有正确配置configChanges
设置,如果Activity销毁重建了,我们还需要保留之前的状态,就需要使用到rememberSaveable
将状态保存到Bundle中,在重建的时候读取数据并恢复状态。先看一个不保存状态的例子:
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
,销毁重建之后还是原来的值。
val countNUm = rememberSaveable{mutableStateOf(0)}
rememberSaveable
会自动将基础类型的数据存储到Bundle
中,如果你需要保存的数据是自定义类型的,比如数据类,你可以使用不同的存储机制,例如使用 Parcelize
注解、使用 listSaver
和mapSaver
等 Compose API,或实现会扩展 Compose 运行时 Saver
类的自定义 Saver
类。
例如使用Parcelize注解:@kotlinx.parcelize.Parcelize
是一个gradle插件,需要导入相关插件
@kotlinx.parcelize.Parcelize
data class City(var name: String, var score: Int) : Parcelable
在onCreate方法中,我们只需要使用rememberSaveable进行状态保存与恢复即可实现
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
可以参考官方的实现。官方文档地址
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?)