注册

Compose 实战经验分享:开发要点&常见错误&面试题

1. 前言

从 Compose 还在 alpha 到现在,用 Compose 完整的从零到一写了三个应用:Twidere X Android、Mask-Android,还有一个暂未公开的项目。

这三个应用每一个都有不一样的收获,现在将开发过程中的经验集中做一波总结:

2. 要点总结

直接说几个总结出来的要点吧:

  1. Compose UI 最核心的一个思想就是:状态向下,事件向上,Compose UI 组件的状态都应该来自其参数而不是自身,不要在 Compose UI 组件中做任何计算,有非常多的性能问题其实是来自对于这一条核心思想的不理解。
  2. 如果一个组件不得不内部持有一些状态,切记将这些状态所有的变量都用上 remember,因为 Compose 函数是会被非常频繁的执行,不用 remember 的话会导致频繁的赋值和初始化,甚至进行一些计算操作。
  3. Compose UI 组件的参数最好是不可变(immutable)的,否则最好的情况是遇到和预期表现不符,最差的情况就是影响到性能了。
  4. 每个 Compose UI 组件最好都有 Modifier,这样 Compose UI 组件就可以很方便的在不同地方复用。
  5. 为了可维护性,请尽量拆分基础 Compose UI 组件和业务 Compose UI 组件,基础 Compose UI 组件尽量拆分的细一些,业务 Compose UI 组件看情况,最好也要拆分的细一些,你不会想去维护一个上千行的 Compose UI 组件的,同时细分也会提高一定的复用率。

下面总结了一些常见的不正确的用法,其中大部分会导致性能问题,有很多人会说 Compose 性能差,但其实更多的是本身的用法有误。

3. 滥用 remember { mutableStateOf() }

Compose UI 最核心的一个思想就是:状态向下,事件向上。这句话举个例子可能会更好理解。 一般初学者在看完教程之后马上就会写下这样的代码:

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    Button(
        onClick = {
            count++
        }
    ) {
        Text("count $count")
    }
}

然后当业务逻辑复杂之后,他的代码可能会像这样:

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    var text by remember { mutableStateOf("") }

    Column {
        Button(
            onClick = {
                count++
            }
        ) {
            Text("count $count")
        }
        TextField(
            value = text,
            onValueChange = {
                text = it
            }
        )
        OtherCounter()
    }
}

@Composable
fun OtherCounter() {
    var text by remember { mutableStateOf("Hello world!") }
    Column {
        Text(text)
        TextField(
            value = text,
            onValueChange = {
                text = it
            }
        )
    }
}

抛开代码的业务逻辑不谈,这里的 Composable 函数是带状态的,这会带来不必要的 recomposition,从而导致写出来的 Compose UI 出现性能问题,按照核心思想状态向下,事件向上,上面的代码应该这样写:

@Composable
fun CounterRoute(
    viewModel: CounterViewModel = viewModel<CounterViewModel>()

) {
    val state by viewModel.state.collectAsState()
    Counter(
        state = state,
        onIncrement = {
            viewModel.onIncrement()
        },
        onTextChange = {
            viewModel.onTextChange(it)
        },
        onOtherTextChange = {
            viewModel.onOtherTextChange(it)
        },
    )
}

@Composable
fun Counter(
    state: CounterState,
    onIncrement: () -> Unit,
    onTextChange: (String) -> Unit,
    onOtherTextChange: (String) -> Unit,
)
 {
    Column {
        Button(
            onClick = {
                onIncrement.invoke()
            }
        ) {
            Text("count ${state.count}")
        }
        TextField(
            value = state.text,
            onValueChange = {
                onTextChange.invoke(it)
            }
        )
        OtherCounter(
            text = state.otherText,
            onTextChange = onOtherTextChange,
        )
    }
}

@Composable
fun OtherCounter(
    text: String,
    onTextChange: (String) -> Unit,
)
 {
    Column {
        Text(text)
        TextField(
            value = text,
            onValueChange = {
                onTextChange.invoke(it)
            }
        )
    }
}

这样的写法吧所有状态都放到顶层,同时事件也交由顶层处理,这样的 Compose UI 组件是没有任何状态的,这样的的 Compose UI 组件会有非常好的性能。

4. 忘记 remember

刚刚说完滥用,现在说忘记。当一个组件不得不内部持有状态的时候,这个时候切记:一定要吧所有的变量都用上 remember。

常见的有这样的错误:

@Composable
fun SomeList() {
    val list = listOf("a""b""c")
    LazyColumn {
        items(list) {
            Text(it)
        }
    }
}

这里的 list 完全没有被 remember,而 Compose 函数会非常频繁的执行,这就导致每次执行到 val list = listOf("a", "b", "c") 的时候都会有一次生成赋值甚至计算的操作,这样的写法是非常影响性能的,正确的写法应该是这样:

@Composable
fun SomeList() {
    val list = remember { listOf("a""b""c") }
    LazyColumn {
        items(list) {
            Text(it)
        }
    }
}

当然最好是把 list 移到参数上:

@Composable
fun SomeList(
    list: List<String>,
)
 {
    LazyColumn {
        items(list) {
            Text(it)
        }
    }
}

5. 参数是可变的

还是接着上一个例子,光是 list 移动到参数还是不够的,因为你可以在 Composable 函数外边更改这个列表,比如执行 list.add("") 的操作,Compose 编译器会认为这个 Composable 函数仍然是带状态的,所以还不是最优化的状态。最好是使用 kotlinx.collections.immutable 里面的 ImmutableList:

@Composable
fun SomeList(
    list: ImmutableList<String>,
)
 {
    LazyColumn {
        items(list) {
            Text(it)
        }
    }
}

除了基础类型之外,其他参数中的自定义 class 最好是标记上 @Immutable,这样 Compose 编译器会优化这个 Composable 函数。当然不要定义一个 data class 然后里面一个 var a: String 然后问为什么 a.a = "b" 没有效果,建议传给 Composable 函数的 data class 全是 val。

6. 没开启 R8

R8 对于 Compose 的提升是非常巨大的,如果是简单 UI 的话没有 R8 可能还可以用,复杂 UI 下非常推荐开启 R8,代码优化之后的性能的 Debug 的性能差距极大。

7. 最后:面试题推荐

其实理解了 Compose UI 的核心思想之后,写出来的 Compose 程序应该不会有什么性能问题,而且在这个核心思想下写出来的 Compose UI 逻辑非常的清晰,因为整个 UI 是无状态的,你只需要关系在什么状态下这个 UI 显示的是什么样的,心智负担非常小。

最后推荐一些 Compose 相关的面试题,大家可以做一个自我测试,如果你能回答的七七八八,那么恭喜你,可能已经击败 95% 的同行了。

  1. Jetpack Compose有了解吗?和传统Android UI有什么不同?
  2. DisposableEffect、SideEffect、LaunchedEffect之间的区别?
  3. pointer事件在各个Composable function之间是如何处理的?
  4. 自定义Layout?
  5. CompositionLocal起什么作用?staticCompositionLocalOf和compositionLocalOf有什么区别?
  6. Composable function的状态是如何持久化的?
  7. LazyColumn是如何做Composable function缓存的?
  8. 如何解决LazyColumn和其他Composable function的滑动冲突?
  9. @Composable的作用是什么?
  10. Jetpack Compose是用什么渲染的?执行流程是怎么样的?与flutter/react那样做diff有什么区别/优劣?
  11. Jetpack Compose多线程执行是如何实现的?
  12. 什么是有状态的 Composable 函数?什么是无状态的 Composable 函数?
  13. Compose 的状态提升如何理解?有什么好处?
  14. 如何理解 MVI 架构?和 MVVM、MVP、MVC 有什么不同的?
  15. 在 Android 上,当一个 Flow 被 collectAsState,应用转入后台时,如果这个 Flow 再进行更新,对应的 State 会不会更新?对应的 Composable 函数会不会更新?

0 个评论

要回复文章请先登录注册