15、Compose 与 View 互操作:AndroidView 嵌入传统 View,ComposeView 在传统布局中使用,Fragment 与 Compose 混合
说实话,Jetpack Compose 出来这么多年了,很多项目还是「新旧混搭」的状态。你不可能一夜之间把整个项目全改成 Compose,对吧?我自己的经验是,大部分团队都是渐进式迁移——新页面用 Compose,老页面继续用 View,两边还得能互相调用。
这一章,我们就来聊聊 Compose 和传统 View 怎么「和平共处」。说白了,就是三个场景:在 Compose 里用 View、在 View 里用 Compose、在 Fragment 里用 Compose。嗯,这三个场景搞明白了,你就能在项目里灵活切换了。
核心要点:Compose 和 View 互操作不是「二选一」,而是「互相嵌套」。AndroidView 和 ComposeView 就是两座桥梁,一个从 Compose 通往 View,一个从 View 通往 Compose。
15.1 为什么需要互操作?
你可能要问:既然 Compose 这么强大,为什么还要回头用 View?
原因其实很简单。我在项目中遇到过好几次这样的情况:某个第三方 SDK 只提供了 View 组件,比如地图控件、视频播放器、WebView 等等。这些组件没有 Compose 版本,你总不能自己重写一个吧?
另外,老项目里可能有大量写好的自定义 View,重构成本太高。渐进式迁移才是明智的选择。
所以,互操作不是「妥协」,而是「务实」。你想想看,一个成熟的 App,哪能说改就全改?
15.2 在 Compose 中使用传统 View:AndroidView
AndroidView 是 Compose 提供的一个 Composable 函数,专门用来把传统 View 嵌入到 Compose 布局中。它的用法非常直观:
@Composable
fun MapViewComposable() {
// 在 Compose 中嵌入一个 MapView
AndroidView(
factory = { context ->
MapView(context).apply {
// 初始化配置
isClickable = true
}
},
update = { mapView ->
// 当状态变化时更新 View
mapView.onResume()
},
modifier = Modifier.fillMaxSize()
)
}
这里有几个关键点,我跟你分享一下我的经验:
- factory 参数:只会在首次组合时调用一次,用来创建 View 实例。别在这里做频繁更新的操作。
- update 参数:每次重组时都会调用,适合更新 View 的状态。比如地图的缩放级别、标记点等。
- modifier 参数:可以像普通 Compose 组件一样设置布局约束。
小技巧:如果你需要在 Compose 中频繁更新 View 的属性,记得把更新逻辑放在 update 块里,而不是 factory 里。我曾经犯过这个错——在 factory 里设置了一次性属性,结果后面状态变了 View 没反应,排查了半天。
15.3 在传统 View 中使用 Compose:ComposeView
反过来,如果你在 XML 布局里想用 Compose,那就需要 ComposeView 了。它本质上是一个 ViewGroup,里面可以承载 Compose 内容。
在 XML 中声明:
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="200dp" />
在 Activity 或 Fragment 中设置内容:
val composeView = findViewById<ComposeView>(R.id.compose_view)
composeView.setContent {
MaterialTheme {
Text(
text = "这是 Compose 内容",
style = MaterialTheme.typography.h5
)
}
}
嗯,这里要注意一个坑:ComposeView 的生命周期。它需要和宿主 Activity 或 Fragment 的生命周期绑定。如果你在 Fragment 里用,建议在 onViewCreated 中设置内容,而不是 onCreateView。
避坑指南:我曾经在 Fragment 的 onCreateView 里直接调用了 composeView.setContent(),结果导致 Compose 内容重复创建,界面闪烁。后来改成在 onViewCreated 中调用,问题就解决了。原因很简单——onCreateView 可能会被多次调用,而 onViewCreated 只调用一次。
15.4 Fragment 与 Compose 混合使用
Fragment 和 Compose 混合,其实有两种常见模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| Fragment 承载 Compose | Fragment 的布局中嵌入 ComposeView | 渐进式迁移,老页面逐步替换 |
| Compose 中使用 Fragment | 在 Compose 中通过 AndroidView 嵌入 FragmentContainerView | 需要复用已有的 Fragment 逻辑 |
我个人更推荐第一种模式——Fragment 作为「壳」,内部用 Compose 实现 UI。这样 Fragment 的生命周期管理照旧,UI 层却可以享受 Compose 的便利。
举个例子:
class MyFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setContent {
MyComposeScreen()
}
}
}
}
你看,Fragment 的 onCreateView 直接返回一个 ComposeView,整个 Fragment 的 UI 就完全由 Compose 接管了。这样 Fragment 的导航、生命周期回调依然有效,但 UI 层已经「Compose 化」了。
我的建议:如果你在做一个新功能,直接用 Compose 写 Fragment 的 UI。如果是在老功能里加新模块,用 ComposeView 嵌入到现有 XML 布局中。这两种方式都不需要动太多现有代码。
15.5 知识体系结构图
下面这张图帮你理清 Compose 和 View 互操作的几种关系:
15.6 实际项目中的最佳实践
说了这么多理论,来点实际的。我在一个电商项目里做过一次迁移,总结了几条经验:
- 从底部开始:先替换那些独立的、不依赖太多业务逻辑的 UI 组件,比如列表项、卡片。
- 保持接口一致:如果 View 和 Compose 需要共享数据,用 ViewModel 做中间层,两边都观察同一个 LiveData 或 StateFlow。
- 注意性能:AndroidView 每次重组都会调用 update 块,如果 update 里做了耗时操作,会影响帧率。我建议把频繁更新的逻辑用 LaunchedEffect 或 SideEffect 隔离。
- 测试要跟上:混合架构的测试比纯 Compose 或纯 View 都复杂。我习惯用 ComposeTestRule 测试 Compose 部分,用 Espresso 测试 View 部分,两边分开测。
避坑指南:我曾经在 Compose 里用 AndroidView 嵌入了一个 RecyclerView,结果发现 RecyclerView 的滚动和 Compose 的滚动冲突了。解决方案是给 AndroidView 设置 Modifier.verticalScroll(rememberScrollState()) 为 disabled,让 RecyclerView 自己处理滚动。
15.7 总结
Compose 和 View 互操作,说白了就是两座桥:AndroidView 让你在 Compose 里用 View,ComposeView 让你在 View 里用 Compose。Fragment 作为中间层,可以灵活地承载 Compose 内容。
我个人觉得,互操作不是「过渡方案」,而是「长期共存方案」。即使未来 Compose 完全成熟了,有些场景(比如地图、视频播放)可能还是需要 View 组件。所以,掌握好这两座桥,你的技术栈会更灵活。
嗯,这一章的内容就到这里。代码不多,但思路很重要。你想想看,如果你的项目里能自由地在 Compose 和 View 之间切换,那迁移起来是不是就轻松多了?