第22章:Compose与View互操作:AndroidView嵌入传统View、ComposeView嵌入Compose到XML、Fragment与Compose混合
说实话,Jetpack Compose 出来之后,很多团队都面临一个现实问题:老项目里几十个 Activity、几百个 Fragment,总不能一夜之间全重写吧?
我当年接手第一个 Compose 项目时,老板就一句话:「新功能用 Compose,老代码别动。」嗯,这就逼着我们必须学会 Compose 和传统 View 系统怎么和平共处。
这一章,我就把三种最常见的互操作场景掰开揉碎讲清楚。你想想看,掌握了这些,你就能在老旧项目里「偷偷」用上 Compose,等时机成熟再逐步替换。
22.1 为什么需要互操作?
说白了,Compose 和 View 是两个不同的渲染体系。Compose 用的是自己的 Canvas 绘制,而传统 View 依赖 ViewGroup 的 measure/layout/draw 流程。
但 Android 框架给它们搭了一座桥——AndroidView 和 ComposeView。这两个组件就是互操作的「翻译官」。
核心原则:
- Compose 里想用老 View → 用
AndroidView - XML 里想嵌入 Compose → 用
ComposeView - Fragment 里混用 → 两种方式都支持
我个人习惯是:能用 Compose 的地方尽量用 Compose,但遇到 MapView、WebView、VideoView 这些「顽固分子」,就老老实实走互操作。
22.2 AndroidView:在Compose中嵌入传统View
这是最常用的场景。比如你要在 Compose 页面里放一个 Google Map,或者一个 RecyclerView。
22.2.1 基本用法
@Composable
fun MapViewInCompose() {
AndroidView(
factory = { context ->
MapView(context).apply {
// 这里配置 MapView
onCreate(Bundle())
onResume()
}
},
modifier = Modifier.fillMaxSize()
)
}
注意看,factory 是一个 lambda,它返回你想要的 View 实例。这个 factory 只会在首次组合时调用一次。
我的经验: factory 里不要做耗时操作。我曾经把网络请求写在 factory 里,结果界面卡顿了好久。后来才意识到,factory 是在主线程执行的。
22.2.2 更新View的状态
很多时候,你需要根据 Compose 的状态来更新 View。这时候要用 update 参数:
@Composable
fun CustomTextView(text: String) {
AndroidView(
factory = { context ->
TextView(context).apply {
textSize = 16f
}
},
update = { textView ->
textView.text = text
}
)
}
update 会在每次重组时被调用。你想想看,这就像给 View 绑定了一个「监听器」,Compose 状态一变,它自动更新。
22.2.3 处理View的生命周期
这里有个坑。MapView、VideoView 这些组件有自己的生命周期方法。你不能光创建不管销毁。
我建议用 DisposableEffect 来管理:
@Composable
fun LifecycleAwareMapView() {
val context = LocalContext.current
AndroidView(
factory = { ctx ->
MapView(ctx).apply {
onCreate(Bundle())
onResume()
}
},
modifier = Modifier.fillMaxSize()
)
DisposableEffect(Unit) {
onDispose {
// 这里清理 MapView
// 比如调用 mapView.onPause()、mapView.onDestroy()
}
}
}
避坑指南: 我曾经忘记在 onDispose 里调用 mapView.onDestroy(),结果导致内存泄漏。MapView 内部持有大量资源,不释放的话,切几次页面就 OOM 了。
22.3 ComposeView:在XML中嵌入Compose
反过来,如果你有一个老旧的 XML 布局,想在里面加一个 Compose 组件,就用 ComposeView。
22.3.1 在XML中声明
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="200dp" />
然后在 Activity 或 Fragment 里:
findViewById<ComposeView>(R.id.compose_view).setContent {
MaterialTheme {
Greeting("来自 Compose 的问候")
}
}
22.3.2 传递数据给Compose
实际项目中,你肯定需要把 XML 那边的数据传给 Compose。我一般这么干:
class MyFragment : Fragment() {
override fun onCreateView(inflater, container, savedInstanceState): View {
return inflater.inflate(R.layout.fragment_mixed, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val composeView = view.findViewById<ComposeView>(R.id.compose_view)
val userName = "张三" // 从老代码获取的数据
composeView.setContent {
UserProfileCard(name = userName)
}
}
}
小技巧: 如果你需要在 Compose 和 View 之间频繁通信,可以考虑用 ViewModel 做桥梁。两边都观察同一个 LiveData 或 StateFlow,这样数据同步很自然。
22.4 Fragment与Compose混合
这是大型项目迁移中最常见的场景。一个 Fragment 里既有老 View,又有 Compose 组件。
22.4.1 在Fragment中使用Compose
最简单的做法:Fragment 的布局里放一个 ComposeView,然后整个界面都用 Compose 写。
class ComposeFragment : Fragment() {
override fun onCreateView(inflater, container, savedInstanceState): View {
return ComposeView(requireContext()).apply {
setContent {
MyComposeScreen()
}
}
}
}
嗯,这样 Fragment 就变成了一个「壳」,里面全是 Compose。
22.4.2 混合布局实战
更常见的是:Fragment 的 XML 布局里既有老 View,又有 ComposeView。比如顶部是传统 Toolbar,中间是 Compose 内容区。
<LinearLayout>
<Toolbar ... /> <!-- 老 View -->
<androidx.compose.ui.platform.ComposeView
android:id="@+id/content_area"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
然后在 Fragment 里:
val composeView = view.findViewById<ComposeView>(R.id.content_area)
composeView.setContent {
// 这里可以访问 Fragment 的 ViewModel
val viewModel = viewModel<MyViewModel>()
MyComposeContent(viewModel = viewModel)
}
注意: ComposeView 的 setContent 只能调用一次。如果你需要更新内容,应该通过重组(recomposition)来实现,而不是重新调用 setContent。
22.5 知识体系总览
下面这张图,是我梳理的 Compose 与 View 互操作的完整知识结构。你看一眼就能明白什么时候该用哪种方式。
22.6 性能与最佳实践
互操作不是免费的。每次 Compose 和 View 之间传递数据,都有一定的性能开销。
| 场景 | 性能影响 | 建议 |
|---|---|---|
| AndroidView 嵌入简单 View | 低 | 放心用 |
| AndroidView 嵌入复杂 View(如 MapView) | 中 | 注意生命周期管理 |
| ComposeView 嵌入到 XML | 低 | 避免频繁重组 |
| Fragment 中大量混合 | 中高 | 尽量统一到 Compose |
我的建议: 如果你在做一个新页面,直接用纯 Compose。只有遇到 MapView、WebView、VideoView、SurfaceView 这些「老顽固」时,才考虑用 AndroidView 包裹。
嗯,这一章的内容就到这里。互操作是 Compose 迁移路上的必修课,掌握了它,你就能在老旧项目里「润物细无声」地引入 Compose。记住:不要追求一步到位,逐步替换才是大项目的生存之道。
公众号:蓝海资料掘金营,微信deep3321