一、概念
Compose将界面(@Composable组合函数)和数据(State状态)分开,事件(Event)会引起状态的变化,进而使用了该状态的组合会发生重组。
MutableState会与引用它的组合函数绑定,当所持有的状态(值)发生变化时通知组合函数进行界面刷新(重组)。
remember( )提供持久化存储功能,因为使用了纯函数形式表达界面,如果在函数中声明属性(局部变量),会因为函数被多次调用而丢失状态(值)。
二、MutableState
Compose专用的观察者模式的容器(同LiveData、StateFlow、Flow、Observable一样可以观察到值的变化)。MutableState会与引用它的组合函数绑定,当所持有的状态(值)发生变化时通知组合函数进行界面刷新(重组)。
- 状态若是一个可变对象(例如MutableList),更改 MutableList 中的 element 不会触发重组,需要的是对象(MutableList)的改变,这是对象内部数据(element)的改变。
引用数据类型 | fun <T> mutableStateOf( value: T, //默认值 policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy() ): MutableState<T> |
List | mutableStateListOf( ) |
Map | mutableStateMapOf( ) |
Number | mutableIntStateOf( ) 避免开装箱产生的性能消耗。 |
ViewModel {
//方式一:使用赋值运算符获取的是MutableState对象,需要.value获取值再进行操作
private val _strState = mutableStateOf<String>("Hello Word!")
val strState = _strState as State<String>
//方式二:使用属性委托直接通过变量名对value进行读写(推荐)
var strState2 by mutableStateOf("")
private set
}
@Composable
fun Input(
viewModel:MainViewModel = viewModel()
) {
Text(text = viewModel.strState.value) //方式一
Text(text = viewModel.strState2) //方式二(推荐)
}
三、remember( )
因为使用了纯函数形式表达界面,如果在函数中声明局部变量,会因为函数被多次调用(重组)而丢失状态(值)。
remreber( )可以为组合函数提供数据持久化的功能,remember( )会记住所修饰的值(存储在UI树中),这样在重组后状态不会被初始化(在有添加或移除元素、重组发生在父元素中、屏幕旋转、恢复前台显示的情况下,使用 remember 会造成数据丢失变成初始值,见下方其它方式存储)。
在组合函数中创建的 MutableState 需要对其进行 remember 操作记住该值,否则每次重组都会初始化成默认值丢失数据,只有在调用方不需要管理状态的时候使用。由于内部创建了状态,难以复用和测试,见下方状态提升。
inline fun <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T 记住Lambda中生成的值(参数不带key,默认key无变化不会重置为始化值)。 |
inline fun <T> remember(key: Any?,crossinline calculation: @DisallowComposableCalls () -> T): T 当key值变化时,就会执行Lambda重新生成值。key值默认不会自己变化,会返回上一次生成的值。 |
//记录点击多少次
@Composable
fun Demo() {
//方式一:使用赋值运算符拿到的是remember()所修饰的MutableState对象。
val count = remember { mutableStateOf(0) }
Button(onClick = { count.value++ }) {
Text(text = "点击了 ${count.value} 次")
}
//方式二:属性委托直接操作变量名来读写value。
//需要导包androidx.compose.runtime.getValue和setValue。
var count by remember { mutableStateOf(0) } //记录点击多少次
Button(onClick = { count++ }) {
Text(text = "点击了 $count 次")
}
//方式三:解构的是某个实现类,value是显示的值,setValue是如何处理新值(事件的处理)。
val (value, setValue) = remember { mutableStateOf("默认值") }
TextField(value = value, onValueChange = setValue)
//通过key控制
val num by remember { mutableStateOf(0) }
val str = remember(key1 = num) { "数字是$num" }
}
四、状态提升 State Hoisting
根据组合项中是否有 State 可分为有状态组合项(Stateful)和无状态组合项(Stateless)。有状态组合项复用性不高难做测试 。
有状态组合变为无状态组合需要状态提升,即将状态(数据源和事件) 移出到组合外给调用方处理,具体是将 State 替换为两个形参:要显示的数据(value),对事件的操作 (T) -> Unit,T是新值。状态提升事件下降即单向数据流设计。
- 状态应至少提升到使用该状态(读取)的所有Composable的最低共同父项。
- 状态应至少提升到它可以发生变化(写入)的最高级别。
- 如果两种状态发生变化以响应相同的事件,它们应该一直提升。
//有状态
@Composable
fun InputWithState() {
val str = remember { mutableStateOf("Hello Word!") }
Column {
Text(text = str.value)
TextField(value = str.value, onValueChange = { str.value = it })
}
}
//状态提升(中间层或底层不必传入ViewModel)
@Composable
fun InputWithoutState(value: String, onValueChange: (String) -> Unit) {
Column {
Text(text = str)
TextField(value = value, onValueChange = onValueChange)
}
}
//ViewModel中处理
var strState = mutableStateOf("Hello Word!")
fun onStrChange(newValue: String) {
strState.value = newValue
}
//Activity中各部分全部组合起来(只在顶层组合传入ViewModel)
@Composable
fun MainActivityScreen(viewMidel:MyViewModel) {
InputWithoutState(viewMidel.strState, viewMidel.onStrChange)
}
五、其它状态存储方式
5.1 rememberSaveable( )
与 remember 类似,用于 Activity 或进程重建时恢复界面状态,类似于 onSaveInstanceState( ),任何可以存储在 Bundle 中的数据都可以通过 rememberSaveable( ) 进行存储。
5.2 @Parcelize
直接对数据类使用该注解,类似于 Java 中的 Serializable 都是将实例编码成字节流存储,但不会产生大量临时对象、没有反射效率更高,也不用写 Parcelable 模板代码。如果不涉及到本地化存储或者网络传输推荐使用。
// 第一步:添加插件
plugins {
id 'kotlin-parcelize'
}
//第二步:添加注解及Parcelable
@Parcelize
data class City(val name: String, val country: String) : Parcelable
//第三步:保存到状态中
val cityBean = rememberSaveable{ mutableStateOf(City("0112","西京"))}
5.3 MapSaver( )
不适合用Parcelize的场景可以使用,定义自己的存储和回复规则,规定如何把实例转为可保存到 Bundle 中的值。通过 save 这个 lambda 可以将 Book 对象转化为一个 Map 进行存储;要使用的时候就通过 restore 这个 lambda 将 Map 又恢复为一个 Book 对象。
data class Book(val name: String, val author: String)
val BookSaver = run {
val nameKey = "Name"
val authorKey = "Author"
mapSaver(
save = { mapOf(nameKey to it.name, authorKey to it.author) },
restore = { Book(it[nameKey] as String, it[authorKey] as String) }
)
}
val chosenBook = rememberSaveable( stateSaver = BookSaver ) {
mutableStateOf(Book("三体","刘慈欣"))
}
5.4 ListSaver( )
List相对于上面的Map不用定义key。
val BookListSaver = listSaver<Book, Any>(
save = { listOf(it.name, it.author) },
restore = { Book(it[0] as String, it[1] as String) }
)
val chosenBook = rememberSaveable( stateSaver = BookSaver ) {
mutableStateOf(Book("三体","刘慈欣"))
}
六、重组性能优化
6.1 使用父组合项中高频变化的状态时采用Lambda传值
由于 Column、Row、Box等是内联函数,编译后不是一个函数,如果内部有读取状态的行为,实际是外层在读取和重组,因此会引发不必要的外层重组。不要在父组合项中读取状态后再传递值给子组合项,而是子组合项的参数类型使用 Lambda,让子组合项读取。简而言之就是尽可能将读取状态的行为延后。
在Compose自带的关于偏移、可见度、大小变化的 API 中都有一个 Lambda 版本,效率更高因为可以跳过重组的过程只重新绘制或布局。
@Preview(showBackground = true)
@Composable
fun DemoPre() {
Column(modifier = Modifier.background(Color.White)) {
Demo1()
Demo2()
}
}
class DemoViewModel : ViewModel() {
var aState by mutableStateOf("A:默认") //A使用属性委托
val bState = mutableStateOf("B:默认") //B使用赋值运算符
var c1State by mutableStateOf("C1:默认") //C1使用属性委托
val c2State = mutableStateOf("C2:默认") //C2使用赋值运算符
fun updateA() { aState = "A:已更新" }
fun updateB() { bState.value = "B:已更新" }
fun updateC1() { c1State = "C1:已更新" }
fun updateC2() { c2State.value = "C2:已更新" }
}
//使用外部传入的状态
@Composable
fun Demo1(
viewModel: DemoViewModel = viewModel()
) {
SideEffect { Log.e("发生重组", "Demo") }
Column {//内联函数,内部读取状态的话,实际是Demo在读取状态,状态变化Demo就会重组
//aState是属性委托方式,调用变量名直接就是读取状态,因此会引发Demo重组
TestA(str = viewModel.aState)
//bState是赋值运算符方式,调用变量名获取的是MutableState对象,不会引发Demo重组
TestB(str = viewModel.bState)
//通过Lambda传参,两种方式都不会引起Demo重组
TestC { viewModel.c1State } //属性委托
TestC { viewModel.c2State.value } //赋值运算符
//值写死,由于没有读取状态,不会有任何重组
TestD(str = "D的值写死")
Button(onClick = {viewModel.updateA()}) { Text(text = "更新A状态") }
Button(onClick = {viewModel.updateB()}) { Text(text = "更新B状态") }
Button(onClick = {viewModel.updateC1()}) { Text(text = "更新C状态-委托模式") }
Button(onClick = {viewModel.updateC2()}) { Text(text = "更新C状态-赋值运算符") }
}
}
//使用内部持有的状态(注释同上,效果一样)
@Composable
fun Demo2() {
var aState by remember { mutableStateOf("A:默认") }
val bState = remember { mutableStateOf("B:默认") }
var c1State by remember { mutableStateOf("C1:默认") }
val c2State = remember { mutableStateOf("C2:默认") }
SideEffect { Log.e("发生重组", "Demo") }
Column {
TestA(str = aState)
TestB(str = bState)
TestC {c1State}
TestC {c2State.value}
TestD(str = "D的值写死")
Button(onClick = { aState = "A:已更新" }) { Text(text = "更新A状态") }
Button(onClick = { bState.value = "B:已更新" }) { Text(text = "更新B状态") }
Button(onClick = { c1State = "C1:已更新" }) { Text(text = "更新C状态-委托模式") }
Button(onClick = { c2State.value = "C2:已更新" }) { Text(text = "更新C状态-赋值运算符") }
}
}
@Composable
fun TestA(str: String) {
SideEffect { Log.e("发生重组", "A") }
Text(text = str)
}
@Composable
fun TestB(str: MutableState<String>) {
SideEffect { Log.e("发生重组", "B") }
Text(text = str.value) //这里调用.value才是读取状态
}
@Composable
fun TestC(str:() -> String) {
SideEffect { Log.e("发生重组", "C") }
Text(text = str()) //这里才是Lambda实际调用的地方(状态读取)
}
@Composable
fun TestD(str: String) {
SideEffect { Log.e("发生重组", "D") }
Text(text = str)
}
6.2 使用派生状态来降低重组次数 derivedStateOf( )
一个状态基于另一个或多个状态得出,即对条件状态经过计算后得出结果状态。可对条件状态进行过滤,避免每次条件状态更新都要连带自己重组。通常使用 remember 的 key 可以实现,有些情况的状态无法用作key,例如元素改变了而 List 没变。
举例一:有一个变化频率非常高的数值,我们只关心正负,正为绿色负为红色。优化前每次数值变化都会导致重组(例如从0.2变成0.3颜色是不变的却进行了重组),优化后只根据正负变化重组。
@Composable
fun Demo() {
var value by remember{ mutableStateOf(0F) }
//监听value的变化派生出isPositive
val isPositive by remember { derivedStateOf { value >= 0 } }
SideEffect { Log.e("发生重组", "Demo") }
Column {
Row {
Button(onClick = { value += 0.1F }) { Text(text = "点击+0.1") }
Button(onClick = { value -= 0.1F }) { Text(text = "点击-0.1") }
}
//这里如果是以 value>=0 为条件,每次数值变化都会引起Demo重组
//将条件换成 isPositive 只有正负变化才会引起Demo重组
Box(modifier = Modifier
.size(50.dp)
.background(if(isPositive) Color.Green else Color.Red) //正数显示绿色,负数显示红色
){
SideEffect { Log.e("发生重组", "Box") }
}
}
}
七、状态转换
7.1 LiveData、Flow、RxJava → State。
LiveData | val liveDataState = liveData.observeAsState() |
Flow | val flowState = flow.collectAsState("str") |
val flowState = flow.collectAsStateWithLifecycle("str") //仅适用于Android,处于后台不收集 //需要导包 implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.6.1' |
implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
implementation "androidx.compose.runtime:runtime-rxjava2:$compose_version"
implementation "androidx.compose.runtime:runtime-flow:$compose_version"
@Composable
fun Demo(
viewModel:DemoViewModel = viewModel()
) {
//LiveData
val dataState: State<String?> = viewModel.data.observeAsState()
Text("Username: ${dataState}")
//Flow
val uiState by viewModel.user.collectAsState(ViewState.Loading)
//RxJava
val uiState by viewModel.user.subscribeAsState(ViewState.Loading)
when (uiState) {
ViewState.Loading -> TODO("Show loading")
ViewState.Error -> TODO("Show Snackbar")
is ViewState.Content -> TODO("Show content")
}
}
class DemoViewModel : ViewModel() {
//LiveData
private val _data = MutableLiveData("")
val data = _data as LiveData<String>
//Flow
// val user: Flow<ViewState> = flowOf(ViewState.Loading) //冷流无意义(后期无法发送值)
val user: MutableStateFlow<ViewState> = MutableStateFlow(ViewState.Loading)
//RxJava
val user: Observable<ViewState> = Observable.just(ViewState.Loading)
}
val countDownTimer = flow {
repeat(5) {
emit(it)
delay(1000)
}
}
@Composable
fun Test() {
//显示5 4 3 2 1
Text("倒计时:${countDownTimer.collectAsState(initial = 10).value}")
}
7.2 协程 → State(produceState)、State → Flow(snapshotFlow)
详见附带效应
八、状态持有者
考虑将状态保存在何处的因素有:界面状态(数据还是UI)、逻辑(业务还是UI)
组合项 | 如果状态数量较少和逻辑比较简单,在组合项中直接增加逻辑和状态是可以的,与其相关的交互都应该在这个组合项进行。但是如果将它传递给其它组合项,就不符合单一可信来源,而且会使调试更困难。 |
状态容器 | 当组合项涉及多个界面的状态等复杂逻辑时,应将相应事务委派给状态容器。这样更易于单独对该逻辑进行测试,还降低了组合的复杂性。保证组合项只是负责展示,而状态容器负责逻辑和状态。 |
ViewModel | ViewModel 的生命周期往往是比较长的,原因是它们在配置发生变化后仍然有效。ViewModel 可以遵循 Activity、Fragment、或导航(如果使用了导航库)的生命周期。正因为 ViewModel 的生命周期较长,因此不应该长期持有和组合密切相关的一些状态,否则,可以会导致内存泄漏。如果 ViewModel 中包含要在进程重建后保留的状态,使用SavedStateHandle。 |
8.1 组合 作为可信来源
val scaffoldState = rememberScaffoldState()
val coroutineScope = rememberCoroutineScope()
Scaffold(scaffoldState = scaffoldState) {
MyContent(
showSnackbar = { message ->
coroutineScope.launch { scaffoldState.snackbarHostState.showSnackbar(message) }
}
)
}
8.2 状态容器 作为可信来源
class MyAppState(
val scaffoldState: ScaffoldState,
val navController: NavHostController,
private val resources: Resources
) {
val bottomBarTabs = /* State */
val shouldShowBottomBar: Boolean
get() {
//何时显示底栏的逻辑
}
fun navigateToBottomBarRoute(route: String) {
//导航逻辑
}
fun showSnackbar(message: String) {
//使用资源显示 snackbar
}
}
//使用MyAppState 的时候需要使用remember来进行信赖
//可以创建一个rememberMyAppState方法来直接返回MyAppState实例。
@Composable
fun rememberMyAppState(
scaffoldState: ScaffoldState = rememberScaffoldState(),
navController: NavHostController = rememberNavController(),
resources: Resources = LocalContext.current.resources,
) = remember(scaffoldState, navController, resources, /* ... */) {
MyAppState(scaffoldState, navController, resources, /* ... */)
}
//使用
val myAppState = rememberMyAppState()
Scaffold(
scaffoldState = myAppState.scaffoldState,
bottomBar = {
if (myAppState.shouldShowBottomBar) {
BottomBar(
tabs = myAppState.bottomBarTabs,
navigateToRoute = { myAppState.navigateToBottomBarRoute(it) }
)
}
}
) {
NavHost(navController = myAppState.navController, "initial") { /* ... */ }
}
8.3 ViewModel 作为可信来源
在函数式编程一大特点就是「不可变性」,即在函数式编程中,状态不是通过修改变量,而是通过创建和返回新的状态来更新的。这意味着数据是不可变的(immutable),一旦创建,就不能更改。通过这种不可变性尽可能的避免副作用。
class LoginVM : ViewModel() {
var uiState by mutableStateOf(LoginUiState())
private set
private fun toLogin() {
uiState = uiState.copy(isFirstRefresh = false) //修改状态
}
}
data class LoginUiState(
val datas: List<VisitorBean> = emptyList(), //为状态提供默认值
val isFirstRefresh: Boolean = true
)
@Composable
fun LoginScreen(
viewModel: LoginVM = viewModel()
) {
if(viewModel.isFirstRefresh) {...} //读取状态
}