第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 框架给它们搭了一座桥——AndroidViewComposeView。这两个组件就是互操作的「翻译官」。

核心原则:

  • 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 互操作的完整知识结构。你看一眼就能明白什么时候该用哪种方式。

Compose 与 View 互操作知识体系 互操作核心 Compose → View AndroidView factory 创建 View update 响应状态变化 DisposableEffect 管理生命周期 View → Compose ComposeView XML 中声明 ComposeView setContent 注入 Compose ViewModel 做数据桥梁 Fragment 混合 Fragment 返回 ComposeView XML + ComposeView 混合 核心原则:能用 Compose 就用 Compose,遇到顽固组件走互操作

22.6 性能与最佳实践

互操作不是免费的。每次 Compose 和 View 之间传递数据,都有一定的性能开销。

场景 性能影响 建议
AndroidView 嵌入简单 View 放心用
AndroidView 嵌入复杂 View(如 MapView) 注意生命周期管理
ComposeView 嵌入到 XML 避免频繁重组
Fragment 中大量混合 中高 尽量统一到 Compose

我的建议: 如果你在做一个新页面,直接用纯 Compose。只有遇到 MapView、WebView、VideoView、SurfaceView 这些「老顽固」时,才考虑用 AndroidView 包裹。

嗯,这一章的内容就到这里。互操作是 Compose 迁移路上的必修课,掌握了它,你就能在老旧项目里「润物细无声」地引入 Compose。记住:不要追求一步到位,逐步替换才是大项目的生存之道。


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