Android Jetpack Compose之UI的重组和自动刷新

1.概述

我们都知道,在传统的View中,若要改变UI,需要我们修改View的私有属性,比如要修改一个TextView的文字,我们需要通过它的setText(“xxx”)方法去修改。而Compose 则是通过重组来刷新UI。在之前的状态管理的文章中也提到过重组的概念。本章主要就是介绍Compose的重组和刷新相关的内容

2.Compose智能重组

compose的重组是很智能的,当重组发生的时候,只有状态发生改变的Composable函数才会参与重组,没有变化的Composable则会跳过本次重组。例如在计数器的demo中:

class ComposeCounterAct : ComponentActivity() {
    
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContent {
    
    
            MyComposeTheme {
    
    
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
    
    
                    CounterComponent()

                }
            }
        }
    }

    @Composable
    fun CounterComponent() {
    
    
        Column(modifier = Modifier.padding(16.dp)) {
    
    
            var counter by remember {
    
     mutableStateOf(0) }
            Text(
                "$counter",
                Modifier.fillMaxWidth(),
                textAlign = TextAlign.Center
            )

            Row {
    
    
                Button(
                    onClick = {
    
     counter-- },
                    modifier = Modifier.weight(1f)
                ) {
    
    
                    Text("-")
                }

                Spacer(Modifier.width(16.dp))
                Button(
                    onClick = {
    
     counter++ },
                    modifier = Modifier.weight(1f)
                ) {
    
    
                    Text("+")
                }
            }
        }
    }
}

当点击Button按钮的时候,counter的状态变化会触发整个Coloum范围的重组。重组过程中显示计数器值的文字组件会被赋予新的counter值,以显示更新后的数字。但是如果此时有另外一个不依赖counter状态的文字组件,则它是不参与重组的,因为Compose编译器会在编译期间插入相关的比较代码,这些比较代码会让没有依赖变更的状态的组件,在对应状态更新时,不参与重组。

有读者可能会疑惑,在Button的onclick方法中也依赖了counter,那它会发生重组吗?答案是不会,因为重组只会在Composable函数中进行,而onClick并非是一个Composable函数,所以和重组无关。此外,Button控件也没有依赖counter状态,所以也不会参与重组

3.避免掉入重组的“坑”

在前面的文章中我们提到过,Composable在编译期间代码会发生变化,所以代码的实际运行情况可能并不如我们预期的那样。所以我们需要了解下Composable在重组执行时的一些特性,避免掉入重组的"坑”。所以我们需要理解掌握下面的内容:

3.1 Composable函数的执行顺序不固定

当我们的代码中出现多个Composable函数时,他们并不一定会按照代码中出现的顺序执行,比如在Navigation中处于Stack最上方的UI会优先被绘制,在一个Box布局中处于前景的UI会具有较高的优先级,因此Composable函数会根据优先级执行。例如:

   @Composable
    fun ButtonRow(){
    
    
        NavigationBar {
    
     
            StartScreen()
            MiddleScreen()
            EndScreen()
        }
    }

如上面的代码所示,在代码中ButtonRow函数一次调用了 StartScreen()、MiddleScreen()、 EndScreen()三个方法,,我们不能预设这三个方法一定是按照顺序执行的。也不要试图去在StartScreen中添加一个全局变量,然后在MiddleScreen()中获取到这个变化,这种关联被称作副作用,在Web前端开发的Vue中也有这个概念。副作用会让我们的UI状态混乱,而且不易维护,所以我们编写Compose时应该尽量避免副作用。

3.2 Composable 会并发执行

Composable在进行重组时不一定执行在主线程中,他们可能在后台线程池中并行执行,这样有利于发挥多核处理器的性能优势,但是由于多个composable在同一时间执行在不同线程,此时必须要注意考虑线程安全的问题。如下面例子所示来自于《Jetpack Compose 实战》一书:

 @Composable
    fun EventsFeed(localEvents:List<Event>,nationalEvent:List<Event>){
    
    
        var totalEvents = 0
        Row{
    
    
            Column {
    
     // 注释1
                localEvents.forEach {
    
     
                    event -> Text("Item: ${
      
      event.name}") 
                    totalEvents ++
                }
            }
            Spacer(modifier = Modifier.height(10.dp))
            Column {
    
     // 注释2
                nationalEvent.forEach {
    
     
                    event -> Text("Item: ${
      
      event.name}") 
                    totalEvents ++
                }
            }
            
            Text(
                if(totalEvents == 0) "No Events." else 
                    "Total events $totalEvents"
            )
        }
    }

在上面的例子中想通过totalEvents记录events的数量并在Text上显示,但是注释1和注释2 处的Column代码有可能在不同的线程中并行执行,所以就导致了totalEvents的累加是非线程安全的,结果可能是不正确的。即使totalEvents的结果正确,但是由于Text可能运行在单独线程,所以也不一定能显示正确的结果,所以这里还是副作用带来的问题,需要我们避开。

3.3 Composable会反复的执行

除了重组会造成Composable的再次执行外,在动画等场景中每一帧的变化都可能引起Composable的执行。所以Composable在短时间内可能会反复的执行,而且我们无法准确的判断它的执行次数。因此我们必须考虑到:即使多次执行也不应该出现性能问题,更不能对外部产生额外有害的影响。同样。借用书中的例子:

 @Composable
    fun EventsFeed(netWorkService: EventsNetWorkService) {
    
    
        val events = netWorkService.loadAllEvents()
        LazyColumn {
    
    
            items(events) {
    
     event ->
                {
    
    
                    Text(text = event.name)
                }
            }
        }
    }

如上面代码所示,在方法EventsFeed中,loadAllEvents是一个IO操作,是一个耗时操作,执行的成本高如果在Composable中同步调用,那么在重组时会造成严重的卡顿问题。所以我们编写代码的时候需要注意Composable会反复的执行,编写UI的时候需要时刻注意这一点

3.4.Composable的执行时“乐观”的

所谓的“乐观”是指Composable最终总会依据最新的状态正确地完成重组。某些场景下,UI状态可能会连续的发生变化,这可能会导致中间态的重组在执行中被打断,新的重组会插入进来。对于被打断的重组,Composable不会将执行一般的重组结果反馈到视图树上,因为它
知道最后一次状态总归是正确的,所以中间的状态丢失了也不影响。

4 总结

本节介绍了Compose的智能重组和刷新,以及重组过程中可能会掉入的“坑”,这些坑我们需要去属性它,因为Compose框架要求Composable作为一个无副作用的纯函数运行,我们只要在开发的过程中遵循这一原则,那么重组中的“坑”就不会再是坑,而是我们提高程序执行性能的有效方法。

猜你喜欢

转载自blog.csdn.net/zxj2589/article/details/132899755