前言
重组是 Compose 核心机制之一。页面和状态的变化依赖函数的执行,换句话,我们也很难确定函数是什么时候执行的,以及它执行的次数。所以,在重组函数中,我们不能按照常规的方法定义状态(如var clickCount = 0),因为重组函数是幂等的。
比如,我们想在一个button上展示点击的次数,我们需要定义一个clickCount存储次数数值,如果直接定义在重组函数体内,那么每一次重组都会导致clickCount的初始化。当然我们可以使用viewmodel来进行存储,但是这种操作不够优雅。
可组合函数作用域之外的应用状态的变化就是指附带效应,简单来说,如果我们希望一个状态或者操作从组合开始到组合结束,无论中间有多少次重组,这个状态都全剧唯一,我们就要使用到这个。
状态
状态最核心的api是remember。
可组合函数可以使用 remember将对象存储在内存中。系统会在初始组合期间将由 remember 计算的值存储在组合中,并在重组期间返回存储的值。
@Composable
fun DetailsDescription(podcast: PodcastInfo, modifier: Modifier) {
var isExpanded by remember { mutableStateOf(false) }
Box(
modifier = modifier.clickable { isExpanded = !isExpanded },
) {
Text(
text = podcast.description,
style = MaterialTheme.typography.bodyMedium,
maxLines = if (isExpanded) Int.MAX_VALUE else 3,
overflow = TextOverflow.Ellipsis,
modifier = modifier,
)
}
}
remember避免了每一次DetailsDescription重组导致isExpanded的重建。mutableStateOf 会创建可观察的 MutableState。在这里isExpanded的值的变化,会触发受isExpanded值影响的代码块的重组。
还有个衍生api : rememberSaveable,它与remember的区别是它将数据存在了saveSavedStateHandle中,这导致在配置变化时,比如旋转屏幕时,数据不会丢失。注意rememberSaveable存储的数据必须可序列化。
我们在实际开发中,常常将viewModel和state联合起来,以生命周期感知型方式从 Flow 收集值,从而使得整个compose更加优雅。
collectAsStateWithLifecycle帮助我们将状态从flow变成了state,从而使使得开发模式更加符合数据的形式。
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
我对Compose的状态的理解,类似于flutter的stateful和stateless控件,因此相比之下,如果能够将重组函数的状态统一管理,能够大大简化底层函数的复杂度。在官方文档中,大力推荐了状态提升的开发模式,即将状态都上移到顶部函数,通过闭包函数的方式作为参数下发给底层函数。这个对于统一状态管理和控件复用,非常有意义。
附带效应
理论上,重组函数适用于UI展示的,但是在很多情况下,我们可能会做一些逻辑操作。比如,网络请求和IO处理。我们不可能每次重组就进行一次逻辑操作,同时这种高性能输出的操作势必需要跟随生命周期,从而避免内存泄漏。
我们可以用LaunchedEffect来启动协程。
@Composable
fun DataLoadingScreen() {
var key by remember { mutableStateOf(true) }
// 当组件进入组合时启动协程加载数据
LaunchedEffect(key) { // 传入Unit表示只执行一次
// 模拟网络请求
}
}
LaunchedEffect的生命周期,从Compose组合开始启动,从组合终止结束,除非key值变化,不然重组时不会再次启动协程。
大部分情况下,LaunchedEffect足以满足我们的业务需求,但是LaunchedEffect生命周期固定,并且只能在Compose函数中使用。因此Compose给我提供了rememberCoroutineScope。它会返回一个 CoroutineScope,该 CoroutineScope 绑定到调用它的组合点。调用退出组合后,作用域将取消。
@Composable
fun execute(viewModel: NetViewModel) {
val scope = rememberCoroutineScope()
viewModel.setCoroutineScope(scope)
Button(onClick = { viewModel.fetchData() }) {
Text("获取数据")
}
}
在上文中,我们提到,状态上升以及状态变化通过闭包函数的下发,是一个推荐的操作。我们无法保证,重组后闭包函数不改变,如果由rember来持有这个闭包函数,就会导致函数是旧的,因此,我们可以使用,rememberUpdatedState。
@Composable
fun TimerComponent(onTick: () -> Unit) {
val latestOnTick by rememberUpdatedState(onTick)
LaunchedEffect(Unit) {
while (true) {
delay(1000)
// 确保每次调用的是最新的 onTick 实现
latestOnTick()
}
}
}
latestOnTick持有的永远是最新的onTick,当然,这里onTick变成String,Int都是一样的。
DisposableEffect用于在键发生变化或可组合项退出组合后进行清理的附带效应。类似于协程的awitCancel()。
DisposableEffect(key) {
// action 1
onDispose {
// action 2
}
}
进入组合执行action1,退出组合时,action 2会执行。需要注意的一点是,如果key变化,会执行action2,在执执行action1,这是一种清除原有状态,重新执行的操作。
设想还有一种情况,我们需要监听每次重组的情况,这个通常和打点有关。SideEffect在每次成功重组后执行相应操作。
@Composable
fun execute(isDarkMode: Boolean) {
// 每次重组后记录主题模式状态
SideEffect {
Log.d("Settings", "当前主题模式:${if (isDarkMode) "深色" else "浅色"}")
}
//.......
}
接下来再说一下其他一些常用Api。
produceState是Compose中将数据转化为state的操作,相比 flow.collectAsState(),produceState 更适合处理单次异步操作(而非持续的数据流)。
@Composable
fun load(userId: String) {
val data by produceState<String>(
initialValue = "开始",
key1 = userId //key
) {
// 模拟网络请求
}
}
除了组合进入和退出外,只有当userId变化时,data会重建。
derivedStateOf用于将一个计算结果包装成一个新的 State 对象。这个通常用于滑动。例如当当前卡片滑动一半时,我们可以触发文案变化,但是时刻监听滚动条,并重组实在是太耗费性能。这时候可以用到derivedStateOf。
@Composable
fun scrollBar() {
val items = (1..100).map { "Item $it" }
// 获取滚动状态
val listState = rememberLazyListState()
// 只有当第一个可见项的索引变化且超过阈值时,才会触发重组
val shouldShowScrollToTop by remember {
derivedStateOf {
// 当列表滚动超过第5项时,显示回到顶部按钮
listState.firstVisibleItemIndex > 5
}
}
Scaffold(
floatingActionButton = {
// 仅当shouldShowScrollToTop变化时才会重组该按钮
if (shouldShowScrollToTop) {
FloatingActionButton(onClick = {
// 滚动到顶部
coroutineScope.launch {
listState.animateScrollToItem(0)
}
}) {
Icon(Icons.Default.ArrowUp, contentDescription = "回到顶部")
}
}
}
) { padding ->
LazyColumn(
state = listState,
contentPadding = padding
) {
items(items) { item ->
ListItem(
headlineContent = { Text(item) },
modifier = Modifier.height(80.dp)
)
}
}
}
}
snapshotFlow可以将state转化为flow,方便我们在协程中处理状态变化,或与其他基于流的逻辑(如数据层的 Flow)进行集成。
@Composable
fun serachText() {
var query by remember { mutableStateOf("") }
val queryFlow = snapshotFlow { query }
// 收集流,添加防抖逻辑
LaunchedEffect(Unit) {
queryFlow
.debounce(300) // 输入停止300ms后才处理
.collect { searchQuery ->
// 执行搜索操作
performSearch(searchQuery)
}
}
//。。。。。
}
这种写法看起来真的很对原生开发的胃口。
总结
目前来看,Compose在和jetpack,协程契合这一块做了非常多的功夫,导致两者调用可以无缝链接。相比于flutter,这个是非常大的优势。