84、Jetpack Compose进阶:列表LazyColumn、动画、主题、与View互操作

各位同学,今天我们来啃一块硬骨头——Compose的进阶玩法。说实话,Compose入门不难,但真正要在项目里用好它,你得掌握几个核心技能:列表怎么高效渲染、动画怎么丝滑、主题怎么统一管理,以及最头疼的——怎么跟老项目里的View共存。

我个人习惯把这四个点称为「Compose四件套」。你在面试时,面试官问Compose,基本就围绕这些来问。好,我们一个一个拆开讲。

一、LazyColumn:别再用Column+ScrollState了

很多新手会犯一个错误:用Column包一堆item,外面套个ScrollState。数据少还好,一旦超过几十条,卡顿就来了。为什么?因为Column会一次性把所有item都创建出来。

LazyColumn不一样。它只渲染当前屏幕可见的item,滚动时回收不可见的item。说白了,它就是RecyclerView的Compose版本。

核心要点: LazyColumn的item必须使用稳定的key,否则列表更新时会闪烁。

来看一个实际例子。我在项目中做过一个联系人列表,每个item包含头像、姓名、电话。用LazyColumn实现是这样的:

@Composable
fun ContactList(contacts: List<Contact>) {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(
            items = contacts,
            key = { it.id }  // 必须提供稳定key
        ) { contact ->
            ContactItem(contact = contact)
        }
    }
}

这里有个坑:key一定要用数据本身的唯一ID,不要用index。我曾经接手过一个项目,用了index作为key,结果用户删除一条数据后,整个列表闪了一下,而且动画错乱。排查了半天,就是key的问题。

小技巧: 如果列表需要添加header或footer,用stickyHeaderitem函数。LazyColumn支持多种item类型,你可以用items传列表,也可以用item插单个组件。

二、动画:从生硬到丝滑,就差这几行代码

Compose的动画系统,说实话,比View那套好用太多了。你不需要写AnimationSet、ObjectAnimator那一堆东西。用animate*AsState系列函数,几行代码搞定。

我常用的几个动画API:

  • animateFloatAsState:数值变化动画,比如进度条、透明度
  • animateColorAsState:颜色过渡,比如按钮按下变色
  • animateContentSize:内容大小变化动画,比如展开/折叠
  • AnimatedVisibility:进场/退场动画,比如弹窗、提示条

举个例子。我之前做一个点赞按钮,需要点击时图标放大再恢复。用Compose写:

@Composable
fun LikeButton(isLiked: Boolean, onToggle: () -> Unit) {
    val scale by animateFloatAsState(
        targetValue = if (isLiked) 1.2f else 1.0f,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )
    IconButton(onClick = onToggle) {
        Icon(
            imageVector = if (isLiked) Icons.Filled.Favorite else Icons.Default.FavoriteBorder,
            contentDescription = "点赞",
            modifier = Modifier.scale(scale),
            tint = if (isLiked) Color.Red else Color.Gray
        )
    }
}

注意那个spring动画规格。我试过用tween,效果太硬。换成spring后,按钮按下去有回弹感,用户体验明显提升。你想想看,一个点赞动画,用户按下去感觉「duang」一下,是不是比生硬缩放舒服多了?

避坑指南: 动画状态不要放在ViewModel里。Compose的动画状态是跟Composable生命周期绑定的,放在ViewModel里会导致状态泄漏。我曾经犯过这个错,结果页面退出后动画还在跑,浪费CPU。

三、主题:别再到处写Color(0xFF...)

很多项目里,颜色、字体、形状散落在各个Composable里。改个主题色要全局搜索替换,太痛苦了。Compose的主题系统就是为了解决这个问题的。

我建议你这样做:

  1. 定义ColorTypographyShapes三个数据类
  2. 创建LightColorSchemeDarkColorScheme
  3. MaterialTheme包裹整个App

看代码:

private val LightColorScheme = lightColorScheme(
    primary = Color(0xFF1976D2),
    onPrimary = Color.White,
    secondary = Color(0xFF03DAC6),
    background = Color(0xFFF5F5F5),
    surface = Color.White,
    error = Color(0xFFB00020)
)

@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
    MaterialTheme(
        colorScheme = colorScheme,
        typography = AppTypography,
        shapes = AppShapes,
        content = content
    )
}

然后在任何Composable里,直接用MaterialTheme.colorScheme.primary获取颜色。改主题?改一个地方就行。

个人经验: 我习惯把AppTheme放在MainActivitysetContent最外层。这样整个App都受主题控制。如果你有部分页面需要不同主题,可以用CompositionLocalProvider局部覆盖。

四、View互操作:老项目迁移的必经之路

这是最实际的问题。你不可能一夜之间把所有页面都改成Compose。大部分项目都是渐进式迁移。那么,Compose和View怎么互相嵌套?

两个方向:

  • Compose里嵌入View:用AndroidView
  • View里嵌入Compose:用ComposeView

先说AndroidView。我在项目中需要嵌入一个WebView,因为Compose没有原生的WebView组件。写法如下:

@Composable
fun WebViewPage(url: String) {
    AndroidView(
        factory = { context ->
            WebView(context).apply {
                settings.javaScriptEnabled = true
                loadUrl(url)
            }
        },
        modifier = Modifier.fillMaxSize()
    )
}

注意factory只会在首次创建时调用。如果你需要更新WebView的状态,用update参数。

反过来,在XML布局里嵌入Compose:

// 在Activity或Fragment中
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.composeView.setContent {
    AppTheme {
        Text("Hello from Compose!")
    }
}

这里有个坑:ComposeView的生命周期需要手动管理。如果你在Fragment里用,记得在onDestroyView里调用composeView.disposeComposition()。我曾经漏掉这一步,导致内存泄漏,页面切来切去后App直接OOM。

重要提醒: Compose和View混用时,事件传递要注意。比如Compose里的点击事件,可能会被外层的View拦截。我建议用Modifier.clickable代替setOnClickListener,保持一致性。

知识体系总览

下面这张图,是我自己总结的Compose进阶知识结构。你可以把它当作学习路线图:

Compose进阶四件套 LazyColumn • 按需渲染,回收不可见item • 必须提供稳定key • 支持stickyHeader • 多种item类型 动画系统 • animate*AsState系列 • AnimatedVisibility • spring/tween动画规格 • 状态绑定Composable生命周期 主题系统 • Color/Typography/Shapes • Light/Dark双主题 • MaterialTheme统一管理 • CompositionLocalProvider View互操作 • AndroidView:Compose里嵌入View • ComposeView:View里嵌入Compose • 生命周期管理 & 事件传递

这四个知识点,说白了就是Compose进阶的四个支柱。你掌握了它们,大部分项目场景都能应对。嗯,今天的内容就到这里。记住,多写多练,代码不会骗你。


公众号:蓝海资料掘金营,微信deep3321