【Compose】一些有用的Compose开发技巧

【Compose】一些有用的Compose开发技巧

本文记录了若干有用的Compose开发技巧

使用Jetpack Compose有很长一段时间了,最近也有在开发跨平台的版本CMP。结合网络上和实际项目的应用,分享几个可以增强性能,提升可读性和可维护性的Compose开发技巧。

状态提升

这个在很多地方都有提到,可以提升Composable的可测试性和可维护性。

具体来说,就是将Composable的状态提升到父Composable中,由父Composable来管理和维护状态,而子Composable只负责展示和交互。这样可以避免子Composable的状态和逻辑与父Composable的状态和逻辑耦合在一起,从而提高代码的可维护性和可测试性。

例如一个简单的计数组件:

@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
    Button(onClick = onIncrement) {
        Text("Clicked $count times")
    }
}

MVI架构设计

这个核心理念用一句话概括就是:数据向下流动,事件向上流动

  • ViewModel组件来管理维护数据状态
  • View层的Composable组件来观测数据
  • 操作和输入事件通过回调的方式向上流动,触发ViewModel的状态更新

还是以计数为例:

// ViewModel
class CounterViewModel : ViewModel() {
    private val _count = mutableStateOf(0)
    val count: State<Int> = _count

    fun increment() {
        _count.value++
    }
}

// View
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
    val count by viewModel.count.collectAsState()
    
    Counter(
      count = count, 
      onIncrement = viewModel::increment
    )
}

插槽化设计

将通用的父组合项抽离出来,将子组合项以插槽的形式传递进去,实现代码的复用和灵活性。

@Composable
fun FancyCard(content: @Composable () -> Unit) {
    Card {
        content()
    }
}

Composed的相当一部分的官方API都是这样设计的。

ViewModel层使用StateFlow作为数据状态

LiveData这个类在Compose中已经不推荐使用了,因为它的设计初衷是为了与传统的Android组件(如Activity和Fragment)进行集成,而Compose是一个基于声明式UI的框架,不依赖于传统的组件生命周期。使用StateFlow可以提供更好的Kotlin适配和灵活性。

class MainViewModel : ViewModel() {
    private val _themeState = MutableStateFlow(ThemeState.DEFAULT)
    val themeStateStateFlow = _themeState.asStateFlow()

    fun setThemeState(themeState: ThemeState) {
        _themeState.value = themeState
    }
}

// View
@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) {
    val themeState by viewModel.themeStateStateFlow.collectAsState()

    LaunchEffect(themeState) {
        // 处理主题状态变化
    }
}

Composables分类

将各个可组合项分类,如状态相关的、布局相关的、样式相关的等,方便管理和维护。有的只需要显示固定的UI,有的是响应数据变化的组件。

// 状态相关的Composable
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
    Button(onClick = onIncrement) {
        Text("Clicked $count times")
    }
}

使用脚手架Scaffold组件

对于移动端来说,界面的布局一般是有固定的部分,如顶部的导航栏、底部的底部栏等。这些部分可以使用Scaffold组件来实现。

@Composable
fun MainScreen() {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Compose Scaffold") }
            )
        },
        bottomBar = {
            BottomAppBar {
                Button(onClick = { /* 处理底部按钮点击 */ }) {
                    Text("底部按钮")
                }
            }
        }
    ) { innerPadding ->
        // 主要内容区域
    }
}

对列表类控件,设置元素的key减少重组,配置动效

列表类控件,如LazyColumnLazyRow等,在Compose中使用时,需要为每个列表项设置一个唯一的key,这样可以帮助Compose在列表项发生变化时,只重新组合发生变化的项,而不是全部重新组合。

LazyColumn {
    items(items = items, key = { item -> item.id }) { item ->
        ListItem(item)
    }
}

有了这个key标识之后,Compose列表项还可以对每一个item组件都应用动效,比如一个元素A前面再插入一个元素B,元素A的位置变化就不是闪现,而是平滑的过渡。注意列表原数据中的每一个元素的key都要求唯一,如果出现了重复的key标识,会报运行错误。

DerivedStateOf

derivedStateOf 的核心作用是只在它的计算结果发生变化时才触发重组。

例如:

// 不推荐
val isFormValid = email.isNotEmpty() && password.length >= 8

在上面的代码中,isFormValid 是一个计算属性,它依赖于 emailpassword 两个变量。当这两个变量发生变化时,就算 isFormValid 的值没有变化,也会触发持有这个属性的外部可组合项发生重组。比如在桌面端,一般会有一个Window可组合项,这个可组合项一个周期内只执行一次,如果在这里触发重组会直接报错。

推荐做法:

// 推荐
val isFormValid by derivedStateOf {
    email.isNotEmpty() && password.length >= 8
}

rememberSaveable

rememberSaveable 是 Jetpack Compose 中用于在配置变更(如屏幕旋转)或进程被系统杀死后保留状态的工具。

它和 remember 很像,但功能更强大:

  • remember 只在重组(recomposition)过程中保留状态。如果用户旋转了屏幕,Activity 被重建,remember 保存的状态就会丢失。
  • rememberSaveable 不仅能在重组时保留状态,还能在 Activity 或进程被销毁和重建时(例如,屏幕旋转、从后台长时间返回)保存状态。
val name by rememberSaveable { mutableStateOf("") }

善用LaunchedEffect

Compose中提供了很多Side Effect方法,用来处理一些副作用,如网络请求、文件读写等。

其中LaunchedEffect是最常用的一个,它可以在Composable中启动一个协程,用来处理一些异步操作。

比如界面的初始化数据获取,会使用Unit作为key,这样只会在Composable第一次被调用时执行一次。

LaunchedEffect(Unit) {
    // 初始化数据获取
}

除此之外,LaunchedEffect还可以用来处理一些变化执行的场景

LaunchedEffect(themeState) {
    // 处理主题状态变化
}

自定义Modifier

一些超高频的Modifier可以自定义,比如点击事件、背景设置、间距设置等。

fun Modifier.defaultPadding() = padding(16.dp)
fun Modifier.defaultClickable() = clickable {
    // 处理点击事件
}