Compose采取了声明式UI的开发范式。在这种范式中,UI的职责仅作为数据状态的反应。如果数据状态没有变化,则UI永远不会自行改变。如果把Composable的执行看作是一个函数运算,那么状态就是函数的参数,生成的布局就是函数的输出。
传统视图中通过获取组件对象句柄来更新组件状态,而Compose则通过重新执行Composable函数来更新页面(重组)。StatelessComposable只依赖参数的Composable;相对的,有些Composable内部持有或者访问了某些状态,称之为StatefulComposable。StatelessComposable的重组只能来自上层Composable的调用,而StatefulComposable的重组来自其以来的状态的变化。
//Statelesscomposable
@Composable
fun Hello(name:String){
Text(text = "Hello $name")
}
//StatefulComposable
@Preview
@Composable
fun CounterComponent() {
Column(
modifier = Modifier.padding(16.dp)
) {
//remember中计算得到的数据会自动缓存,当Composable重组再次执行到remember处会返回之前已缓存的数据,无须重新计算。mutableStateOf的调用一定要出现在remember中,不然每次重组都会创建新的状态。
var counter by remember { mutableStateOf(0) }
Text( //1
"Click the buttons to adjust your value:",
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
Text( //2
"$counter",
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
style = typography.h3
)
Row {
Button(
onClick = { counter-- },
Modifier.weight(1f)
) {
Text("-")
}
Spacer(Modifier.width(16.dp))
Button(
onClick = { counter++ },
Modifier.weight(1f)
) {
Text("+")
}
}
}
}
在Compose中使用State
interface State<out T> {
val value: T
}
State
有时候Composable需要对State的value进行修改,比如在CounterComponent中单击按钮需要修改counter的值,所以counter可被修改,使用MutableState
@Stable
interface MutableState<T> : State<T> {
override var value: T
operator fun component1(): T
operator fun component2(): (T) -> Unit
}
MutableState有三种用法:
创建MutableState
var counter:MutableState<Int> = mutableStateOf(0)
解构方式
val(counter,setCounter) = mutableStateOf(0)
此时的counter已经是一个Int类型的数据,后续使用的地方可以直接访问,无须再使用点操作符获取value,而需要更新counter的地方可以使用setCounter(xx)完成。
属性代理
使用by关键字直接获取Int类型counter。
var counter by mutableStateOf(0)
by关键字的原理是对counter的读写会通过getValue和setValue这两个运算符的重写最终代理为对value的操作。
状态上提的通常做法是将内部状态移除,通过参数传入需要在UI显示的状态,以及需要回调给调用方的事件,案例如下所示:
@Composable
fun CounterComponent(
counter:Int,//重组时调用方传入当前需要显示的计数
onIncrement:()->Unit,//向调用方回调单击加号的事件
onDecrement:()->Unit,//向调用方回调单击减号的事件
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text( //1
"Click the buttons to adjust your value:",
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
Text( //2
"$counter",
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
style = typography.h3
)
Row {
Button(
onClick = { onDecrement() },
Modifier.weight(1f)
) {
Text("-")
}
Spacer(Modifier.width(16.dp))
Button(
onClick = { onIncrement() },
Modifier.weight(1f)
) {
Text("+")
}
}
}
}
前面说到,remember可以缓存创建的状态,避免因为重组而丢失。使用remember缓存的状态虽然可以跨越重组,但是不能跨越Activity或跨越进程存在。如果想要更长久地保存状态,就需要使用到rememberSaveable了,它可以像Activity的onSaveInstanceState那样在进程被杀死时自动保存状态,同时onRestoreInstanceState一样随进程重建而自动恢复。
rememberSaveable中的数据会随onSaveInstanceState进行保存,并在进程或Activity重建时根据key恢复到对应的Composable中,这个key就是Composable在编译期被确定的唯一标识。因此当用户手动退出应用时,rememberSavable中的数据才会被清空。
rememberSaveable实现原理实际上就是将数据以Bundle的形式保存,所以凡是Bundle支持的基本数据类型都可以自动保存。对于一个对象类型,则可以通过添加@Parcelize变为一个Parcelable对象进行保存。当我们遇到有的数据结构可能无法添加Parcelable接口,此时可以通过自定义Saver为其实现保存和恢复的逻辑。只需要在调用rememberSaveable时传入此Saver即可:
@Parcelize
data class City(val name: String,val country:String):Parcelable
object CitySaver:Saver<City,Bundle>{
override fun restore(value: Bundle): City? {
return value.getString("name")?.let {name ->
value.getString("country")?.let { country->
City(name = name,country=country)
}
}
}
override fun SaverScope.save(value: City): Bundle? {
return Bundle().apply {
putString("name",value.name)
putString("country",value.country)
}
}
}
@Preview
@Composable
fun WelcomePageLightPreview() {
WelcomePage()
val city = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("City",""))
}
}
除了自定义Saver外,Compose也提供了MapSaver和ListSaver供开发者使用,MapSaver将对像转换为Map
val cityMapSaver = run {
val nameKey = "Name"
val countryKey = "Country"
mapSaver(
save = { mapOf(nameKey to it.name,countryKey to it.country) },
restore = {City(it[nameKey] as String,it[countryKey] as String)}
)
}
val cityListSaver = run {
val nameKey = "Name"
val countryKey = "Country"
listSaver(
save = { listOf(it.name,it.country) },
restore = {City(nameKey,countryKey)}
)
}
各位读者可以参考下面这两篇博客:
https://juejin.cn/post/7105236042864656391
https://blog.csdn.net/u010976213/article/details/117284079
Compose的重组非常“智能”,当重组发生时,只有状态发生更新的Composable才会参与重组,没有变化的Composable会跳过本次重组。
由于Composable在编译期代码会发生变化,代码的实际运行情况可能不如预期的那样。所以需要了解composable在重组执行时的一些特性,避免陷入重组的“陷阱”。
Composable会以任意顺序执行
多个composable函数未必按照先后顺序执行,因此不能在composable设置一个全局变量,以期望在第一个composable中修改该值,在第二个composable修改为另一个值。
Composable会并发执行
重组中的Composable并不一定执行在UI线程,它们可能在后台线程池中并行执行,这有利于发挥多核处理器的性能优势。
Composable会反复执行
除了重组会造成Composable的再次执行外,在动画等场景中每一帧的变化都可能引起Composable的执行,因此Composable有可能会短时间内反复执行,我们无法准确判断它的执行次数。
Composable的执行是“乐观”的
所谓“乐观”是指composable最终总会依据最新的状态正确地完成重组。在某些场景下,状态可能会连续变化,这可能会导致中间态的重组在执行中被打断,新的重组会插入进来。对于被打断的重组,composable不会将执行一般的重组结果反应到视图树上,因为他知道最后一次状态总归是正确的,因此中间态丢弃也没关系。
总结:composable框架要求composable作为一个无副作用的纯函数运行,只要在开发中遵循这一原则,上述这一系列特性就不会成为程序执行的“陷阱”,反而有助于提高程序的执行性能。
经过composable编译器处理后的Composable代码在对state进行读取的同时,能够自动建立关联,在运行过程中当state变化时,Compose会找到关联的代码块标记为Invalid。在下一渲染帧到来之前,Compose会触发重组并执行invalid代码块,Invalid代码块即下一次重组的范围。能够被标记为Invalid的代码必须是非inline且无返回值的composable函数或lambda。因为inline函数在编译期间会在调用处展开,因此无法在下次重组时找到合适的调用入口,只能共享调用方的重组范围。而对于有返回值的函数,由于返回值的变化会影响调用方,所以必须连同调用方一同参与重组,因此它不能单独作为Invalid代码块。
在编译期间,compose编译器会根据代码调用位置,为composable生成索引key,并存入composition。composable在执行过程中通过与key的对比,可以知道当前应该执行何种操作(增、删、更新、移动等多种变化)。
compose的dsl很形象地描述了UI的视图结构,其背后对应这一视图树的结构体,我们称之为Composition。Composition在Composable初次执行时被创建,在Composable中访问State时,Composition记录其引用,当State变化时,Composition触发对应的Composable进行重组,更新视图树的节点,显示中的UI得到刷新。
Composable在执行过程中,凡是会影响外界的操作都属于副作用,比如弹出Toast、保存本地文件、访问远程或本地数据等。我们知道,重组可能会造成Composable反复执行,副作用显然是不应该跟随重组反复执行的。为此,Compose提供了一系列副作用API,可以让副作用API只发生在Composable生命周期的特定阶段,确保行为的可预期性。
DisposableEffect
该api可以感知composable的onactive和ondispose,允许通过副作用完成一些预处理和收尾处理。DisposableEffect想rememner一样可以接受观察参数key,但是它的key不能为空。如果key为Unit或true这样的常量,则block只在onactive时执行一次;如果key为其他变量,则block在onactive以及参数变化时的onupdate中执行。DisposableEffect的最后必须跟随一个onDispose代码块,否则会编译错误。 onDispose常用来做一些副作用的收尾处理。当有新的副作用会执行onDispose,此外当Composable进入onDispose时,也会执行。
@Composable
fun DisposableEffectTest(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
) {
val inputText = remember { mutableStateOf("") }
Log.e("DisposableEffectTest","Composed")
DisposableEffect(inputText.value) {
// Create an observer that triggers our remembered callbacks
// for sending analytics events
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
Log.e("DisposableEffectTest","ON_START")
} else if (event == Lifecycle.Event.ON_STOP) {
Log.e("DisposableEffectTest","ON_STOP")
}
}
// Add the observer to the lifecycle
lifecycleOwner.lifecycle.addObserver(observer)
// When the effect leaves the Composition, remove the observer
onDispose {
Log.e("DisposableEffectTest","onDispose")
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
Scaffold() {
Column(modifier = Modifier.padding(it)) {
Button(onClick = {
inputText.value = "按了一下"
}) {
Text(text = "按钮"+inputText.value)
}
}
}
}
SideEffect
SideEffect在每次成功重组时都会执行,所以不能用来处理那些好事或者异步的副作用逻辑。因为SideEffect能够获取与Composable一致的最新状态,它可以用来将当前State正确地暴露外外部。
@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {
/* ... */
}
// On every successful composition, update FirebaseAnalytics with
// the userType from the current User, ensuring that future analytics
// events have this metadata attached
SideEffect {//将状态通知外部
analytics.setUserProperty("userType", user.userType)
}
return analytics
}
LaunchedEffect
当副作用中有处理异步任务的需求时,可以使用LaunchedEffect。在Composable进入onactive时,LaunchedEffect会启动协程执行block中的内容,可以在其中启动子协程或调用挂起函数。当Composable进入OnDispose时,协程会自动取消,因此LaunchedEffect不需要实现OnDispose{…}。
LaunchedEffect支持观察参数key的设置,当key发生变化时,当前协程自动结束,同时开启新协程。
@Composable
fun LaunchedEffectTest() {
val state = remember {
mutableStateOf("xiaomi")
}
LaunchedEffect(state){
Log.e("LaunchedEffectTest", "request")
delay(3000)//模拟网络操作
state.value = "oppo"
}
Log.e("LaunchedEffectTest", state.value)
Scaffold{
Column(modifier = Modifier.padding(it)) {
Spacer(modifier = Modifier.padding(top = 50.dp))
Button(onClick = {
state.value = "vivo"
}) {
Text(text = "按钮")
}
Spacer(modifier = Modifier.padding(top = 100.dp))
Text(text = "手机品牌 ${state.value}")
}
}
}
rememberCoroutineScope
LaunchedEffect只能在Composable中调用,如果想在非Composable环境中使用协程,例如在Button的Onclick中使用协程,并希望其在OnDispose时自动取消,此时可以使用rememberCoroutineScope。
@Composable
fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) {
// Creates a CoroutineScope bound to the MoviesScreen's lifecycle
val scope = rememberCoroutineScope()
Scaffold(scaffoldState = scaffoldState) {
Column {
/* ... */
Button(
onClick = {
// Create a new coroutine in the event handler to show a snackbar
scope.launch {
//Thread.sleep(1000)
scaffoldState.snackbarHostState.showSnackbar("Something happened!")
}
}
) {
Text("Press me")
}
}
}
}
rememberUpdatedState
LaunchedEffect会在参数key变化时启动一个协程,但有时我们并不希望协程中断,只要能够实时获取最新状态即可,此时可以借助rememberUpdatedState实现。
@Composable
fun UpdatedStateTest() {
var message= remember { mutableStateOf("start") }
Scaffold { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)){
Button(
onClick = {
message.value = "clicked"
}
) {
Text("描述信息")
}
LoadingScreen(message.value)
}
}
}
@Composable
fun LoadingScreen(text: String,scaffoldState: ScaffoldState = rememberScaffoldState()) {
val messageText by rememberUpdatedState(text)
Log.e("LoadingScreen", "start")
LaunchedEffect(true) {
Log.e("LoadingScreen", "delay origin ${messageText}")
delay(4000)
Log.e("LoadingScreen", "delay remember ${messageText}")
scaffoldState.snackbarHostState.showSnackbar(
message = "切换了方法",
actionLabel = messageText
)
}
Scaffold(scaffoldState = scaffoldState) {
Column(modifier = Modifier.padding(it)) {
}
}
}
snapshotFlow
LaunchedEffect中可以通过rememberUpdatedState获取最新状态,但是当状态发生变化时,LaunchedEffect无法第一时间受到通知,如果通过改变观察参数key来通知状态变化,则会中断当前执行中的任务,成本太高。简言之,LaunchedEffect缺少轻量级的观察状态变化的机制。
@Composable
fun SnapshotFlow() {
Box(modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center) {
val listState = rememberLazyListState()
LazyColumn(state = listState) {
items(500) { index ->
Text(text = "Item: $index")
}
}
Log.e("SnapshotFlow", "Recompose")
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 4 }
.distinctUntilChanged()
.filter { it }
.collect {//经过snapshotFlow 转换的flow是个冷流,只有在collect之后,block才开始执行。
Log.e("SnapshotFlow", "snapshotFlow${it}")
}
}
}
}
当一个LaunchedEffect中依赖的State会频繁变化时,不应该使用State的值作为key,而应该将State本身作为key,然后在其内部使用snapshotFlow 依赖状态。使用state作为key是为了当state对象本身变化时重启副作用。
produceState
produceState会启动一个协程,和SideEffect相反,使用此协程可以将非Compose状态转换为Compose状态。
@Composable
fun loadNetworkImage(
url: String,
imageRepository: ImageRepository
): State<Result<ImageBitmap>> {
Log.e("ProduceStateExample", "loadNetworkImage: invoke" )
//当url和imageRepository发生变化时,producer会重新执行
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading,url, imageRepository) {
// value = Result.Loading
val image = imageRepository.loadNetworkImage(url)
//value 为 MutableState 中的属性
value = if (image == null) {
Result.Error
} else {
Result.Success(image)
}
}
}
//密封类
sealed class Result<T>() {
object Loading : Result<ImageBitmap>()
object Error : Result<ImageBitmap>()
data class Success(val image: ImageBitmap) : Result<ImageBitmap>()
}
derivedStateOf
derivedStateOf用来将一个或多个State转成另一个State。derivedStateOf{…}的block中可以依赖其他State创建并返回一个DerivedState,当block依赖的State发生变化时,会更新此DerivedState,依赖此DerivedState的所有Composable会因其变化而重组。
@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {
val todoTasks = remember { mutableStateListOf<String>("huawei", "xiaomi", "oppo", "apple", "Compose") }
// 选择 todoTasks中 属于 highPriorityKeywords 的部分
val highPriorityTasks by remember(highPriorityKeywords) {
derivedStateOf { todoTasks.filter { highPriorityKeywords.contains(it) } }
}
Log.e("TodoList", "todoTasks:${todoTasks.toList().toString()}" )
Log.e("TodoList", "highPriorityTasks:${highPriorityTasks.toList().toString()}" )
Column(modifier = Modifier.fillMaxWidth()) {
LazyColumn {
item {
Text(text = "add-TodoTasks", Modifier.clickable {
todoTasks.add("Review")
})
}
item {
Divider(
color = Color.Red, modifier = Modifier
.height(2.dp)
.fillMaxWidth()
)
}
items(highPriorityTasks) { Text(text = it) }
item {
Divider(
color = Color.Red, modifier = Modifier
.height(2.dp)
.fillMaxWidth()
)
}
items(todoTasks) {
Text(text = it)
}
}
}
}
derivedStateOf只能监听block内的state,一个非state类型数据的变化则可以通过remember的key进行监听,如上例所示。
注意:在statefulcomposable中创建状态时,需要使用remember包裹,状态只在onactive时创建一次,不跟随重组反复创建,所以remember本质上也是一种副作用api。
副作用API的观察参数
不少副作用api都允许指定观察参数key。当观察参数变化时,执行中的副作用会终止,key的频繁变化会影响执行效率。反之,如果副作用中存在可变值,但没有指定为key,有可能因为没有及时响应变化而出现bug。因此,关于参数key的添加可以遵循以下原则:当一个状态的变化需要造成副作用终止时,才将其添加为观察参数key,否则应该将其使用rememberUpdatedState包装后,在副作用中使用,以避免打断执行中的副作用。