Compose Multiplatform 共享边界实战:架构契约、互操作同步与可回退设计

摘要 :CMP 1.8.0 稳定版解决了"能不能用于生产"的准入门槛,但没有回答"共享多深、边界在哪、性能怎么验收、回退是否真实可用"。本文围绕三个工程问题展开:共享边界画在哪里、互操作层的同步契约是否被正确实现、性能问题出现时能否归因到正确的阶段。 边界层面给出 commonMain 的硬性判定标准(原样编译到 iOS 与 wasmJs 零修改通过)、StateHolder + StateFlow 的状态契约,以及 SharedKernel 边界层"定义契约而非胶水"的作用;互操作层面把 AndroidView / UIKitView 还原为两套渲染管线之间的同步契约,逐一拆解双重布局、手势与焦点双向同步、不稳定 lambda 触发原生视图重建三类问题及修复代码;性能层面以 TechnoAlpin 的完整归因链说明"重组次数为零而帧率个位数"如何被定位到绘制阶段,并给出同设备原生基线、越界动作与 Web 端独立建基线的验收方法;最后落到宿主接入与降级:Android 的 setContent / ComposeView、iOS 的 ComposeUIViewController + UIViewControllerRepresentable、宿主级特性开关与单页回退,以及停止优化的判断线。

目录

  • 前言
  • [一、共享边界:commonMain 的硬性规则与状态归属](#一、共享边界:commonMain 的硬性规则与状态归属 "#%E4%B8%80%E5%85%B1%E4%BA%AB%E8%BE%B9%E7%95%8Ccommonmain-%E7%9A%84%E7%A1%AC%E6%80%A7%E8%A7%84%E5%88%99%E4%B8%8E%E7%8A%B6%E6%80%81%E5%BD%92%E5%B1%9E")
    • [1.1 边界画错的三种典型形式](#1.1 边界画错的三种典型形式 "#11-%E8%BE%B9%E7%95%8C%E7%94%BB%E9%94%99%E7%9A%84%E4%B8%89%E7%A7%8D%E5%85%B8%E5%9E%8B%E5%BD%A2%E5%BC%8F")
    • [1.2 StateHolder 模式:commonMain 的安全状态契约](#1.2 StateHolder 模式:commonMain 的安全状态契约 "#12-stateholder-%E6%A8%A1%E5%BC%8Fcommonmain-%E7%9A%84%E5%AE%89%E5%85%A8%E7%8A%B6%E6%80%81%E5%A5%91%E7%BA%A6")
    • [1.3 边界层的真实作用:契约,而非胶水](#1.3 边界层的真实作用:契约,而非胶水 "#13-%E8%BE%B9%E7%95%8C%E5%B1%82%E7%9A%84%E7%9C%9F%E5%AE%9E%E4%BD%9C%E7%94%A8%E5%A5%91%E7%BA%A6%E8%80%8C%E9%9D%9E%E8%83%B6%E6%B0%B4")
  • 二、互操作:不是"包装",是同步契约
    • [2.1 为什么"包装"的直觉是错的](#2.1 为什么“包装”的直觉是错的 "#21-%E4%B8%BA%E4%BB%80%E4%B9%88%E5%8C%85%E8%A3%85%E7%9A%84%E7%9B%B4%E8%A7%89%E6%98%AF%E9%94%99%E7%9A%84")
    • [2.2 问题一:双重布局与帧预算](#2.2 问题一:双重布局与帧预算 "#22-%E9%97%AE%E9%A2%98%E4%B8%80%E5%8F%8C%E9%87%8D%E5%B8%83%E5%B1%80%E4%B8%8E%E5%B8%A7%E9%A2%84%E7%AE%97")
    • [2.3 问题二:手势与焦点的双向同步](#2.3 问题二:手势与焦点的双向同步 "#23-%E9%97%AE%E9%A2%98%E4%BA%8C%E6%89%8B%E5%8A%BF%E4%B8%8E%E7%84%A6%E7%82%B9%E7%9A%84%E5%8F%8C%E5%90%91%E5%90%8C%E6%AD%A5")
    • [2.4 问题三:不稳定 lambda 导致原生视图重建](#2.4 问题三:不稳定 lambda 导致原生视图重建 "#24-%E9%97%AE%E9%A2%98%E4%B8%89%E4%B8%8D%E7%A8%B3%E5%AE%9A-lambda-%E5%AF%BC%E8%87%B4%E5%8E%9F%E7%94%9F%E8%A7%86%E5%9B%BE%E9%87%8D%E5%BB%BA")
  • 三、性能:先归因到阶段,再决定优化还是回退
    • [3.1 TechnoAlpin 案例的完整归因链](#3.1 TechnoAlpin 案例的完整归因链 "#31-technoalpin-%E6%A1%88%E4%BE%8B%E7%9A%84%E5%AE%8C%E6%95%B4%E5%BD%92%E5%9B%A0%E9%93%BE")
    • [3.2 基线与验收](#3.2 基线与验收 "#32-%E5%9F%BA%E7%BA%BF%E4%B8%8E%E9%AA%8C%E6%94%B6")
    • [3.3 Web 端不能复用移动端基线](#3.3 Web 端不能复用移动端基线 "#33-web-%E7%AB%AF%E4%B8%8D%E8%83%BD%E5%A4%8D%E7%94%A8%E7%A7%BB%E5%8A%A8%E7%AB%AF%E5%9F%BA%E7%BA%BF")
  • 四、宿主接入与降级:让回退成为可验证的路径
    • [4.1 Android:setContent / ComposeView 作为唯一接缝](#4.1 Android:setContent / ComposeView 作为唯一接缝 "#41-androidsetcontent--composeview-%E4%BD%9C%E4%B8%BA%E5%94%AF%E4%B8%80%E6%8E%A5%E7%BC%9D")
    • [4.2 iOS:ComposeUIViewController + UIViewControllerRepresentable](#4.2 iOS:ComposeUIViewController + UIViewControllerRepresentable "#42-ioscomposeuiviewcontroller--uiviewcontrollerrepresentable")
    • [4.3 降级:宿主级开关与单页回退](#4.3 降级:宿主级开关与单页回退 "#43-%E9%99%8D%E7%BA%A7%E5%AE%BF%E4%B8%BB%E7%BA%A7%E5%BC%80%E5%85%B3%E4%B8%8E%E5%8D%95%E9%A1%B5%E5%9B%9E%E9%80%80")
    • [4.4 停止优化的判断线](#4.4 停止优化的判断线 "#44-%E5%81%9C%E6%AD%A2%E4%BC%98%E5%8C%96%E7%9A%84%E5%88%A4%E6%96%AD%E7%BA%BF")
  • [五、CMP 不适用的场景](#五、CMP 不适用的场景 "#%E4%BA%94cmp-%E4%B8%8D%E9%80%82%E7%94%A8%E7%9A%84%E5%9C%BA%E6%99%AF")
  • 六、结论

前言

CMP 1.8.0 的数据已经说明"可以用":启动与原生相当,滚动与 SwiftUI 持平,包体积增量约 9 MB,96% 以上的 iOS 端开发者报告没有重大性能问题。但这是准入门槛,不是工程决策。真正决定迁移成败的是另外三件事:共享边界画在哪里、互操作层的同步契约是否被正确实现、性能问题出现时能否归因到正确的阶段,而非在错误的层面上打补丁。

Sony 和 TechnoAlpin 的案例只用来提取可复用的技术模式,不作为论据:前者展示一个四语言 UI 栈如何在不对遗留代码做重写的前提下被替换,后者展示"重组次数为零但帧率只有个位数"如何被精确归因。代码和配置以可验证为准,阈值以团队自身原生基线替代。

一、共享边界:commonMain 的硬性规则与状态归属

1.1 边界画错的三种典型形式

CMP 迁移的多数问题不是框架性能不足,而是边界定义在源头就有缺陷,典型的有三种形式。

下图概括 commonMain 与各平台源集之间的边界关系,以及平台 API 泄漏如何破坏这条边界:

flowchart TD subgraph C[&#34;commonMain(共享边界)&#34;] A[&#34;SettingsStateHolder<br/>StateFlow 状态契约&#34;] B[&#34;SettingsScreen<br/>无状态 Composable&#34;] A --> B end subgraph P[&#34;平台源集&#34;] D[&#34;androidMain<br/>setContent / ComposeView&#34;] E[&#34;iosMain<br/>ComposeUIViewController&#34;] F[&#34;wasmJsMain<br/>ComposeViewport&#34;] end B --> D B --> E B --> F G[&#34;android.* / LocalContext / Hilt&#34;] -.->|&#34;泄漏即边界失效&#34;| C D --> H[&#34;原生兜底实现<br/>不 import shared&#34;] E --> H

共享层依赖平台 API。 commonMain 中出现 android.* 导入、LocalContext.current 或 Android 的 ViewModel,编译到 iOS 或 Wasm 时失败,或者被迫用大量 expect/actual 包裹,共享层退化为"平台差异的分发中心"。判定标准很简单:把 commonMain 的代码原样编译到 iOS 和 wasmJs,是否零修改通过。 答案是否定的,就说明平台 API 泄漏进了共享层。

状态归属错误。 UI 状态由各端宿主各自持有,双端行为在迭代中逐渐分叉,最终共享的只是一层无状态的壳。正确做法是:状态逻辑放在 commonMain 中,以不可变 UiState 和 StateFlow 暴露,平台层只负责生命周期感知的收集。

降级点与共享层耦合。 所谓的"原生兜底实现"引用了共享模块的类型,退回原生时只是切换到共享模块的另一个函数,回退路径在架构上不存在。

1.2 StateHolder 模式:commonMain 的安全状态契约

在 commonMain 中使用 ViewModel 需要 org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose 2.10.0 及以上,并且 viewModel { } 必须提供初始化器------无参的 viewModel<T>() 在非 JVM 目标上会失败。如果不愿引入额外的 lifecycle 构件,更轻量且完全跨平台的模式是 StateHolder + StateFlow:

kotlin 复制代码
// commonMain
class SettingsStateHolder(
    private val repository: SettingsRepository,
) {
    private val _uiState = MutableStateFlow(SettingsUiState())
    val uiState: StateFlow<SettingsUiState> = _uiState.asStateFlow()

    fun toggleNotifications() {
        _uiState.update { it.copy(notificationsEnabled = !it.notificationsEnabled) }
    }
}

data class SettingsUiState(
    val userName: String = "",
    val notificationsEnabled: Boolean = true,
)

共享 Composable 只接收状态和事件,保持无状态:

kotlin 复制代码
@Composable
fun SettingsScreen(
    state: SettingsUiState,
    onToggleNotifications: () -> Unit,
    onOpenNativePage: () -> Unit,
    onLogout: () -> Unit,
) {
    Column(Modifier.fillMaxSize().padding(16.dp)) {
        Text("当前用户:${state.userName}")
        Text("通知:${if (state.notificationsEnabled) "已开启" else "已关闭"}")
        Button(onClick = onToggleNotifications) { Text("切换通知开关") }
        Button(onClick = onOpenNativePage) { Text("打开原生设置页") }
        Button(onClick = onLogout) { Text("退出登录") }
    }
}

依赖注入的边界。 Hilt 依赖 kapt/Java 注解处理,只能在 androidMain 中使用;共享模块的 DI 应选用 Koin 或 Metro,二者都能在 commonMain 工作。原则是:平台侧注入平台特有的实现,共享层通过接口消费。

1.3 边界层的真实作用:契约,而非胶水

Sony Auto Play 的 0.2% SharedKernel 边界层常被引用为"零重写迁移"的范例,但更值得看的是它定义了什么:哪些 UI 由 CMP 渲染、哪些仍由遗留 J2ObjC 代码渲染;遗留层向共享层暴露哪些能力;数据与导航的边界在哪里。

这个模式的前提是模块可切割。如果存量应用的 UI 与业务逻辑深度交织、划不出可隔离的模块边界,SharedKernel 无法复制------它是适用边界,不是通用公式。

二、互操作:不是"包装",是同步契约

2.1 为什么"包装"的直觉是错的

多数团队把 AndroidView 和 UIKitView 理解为简单的包装器,这个理解会导致生产环境中的掉帧和手势失效。它们的真实身份是两套渲染管线之间的同步契约 :Compose 使用保留模式的 LayoutNode 场景图,原生视图使用即时模式的布局系统(iOS 的 Auto Layout 约束求解、Android 的 measure/layout 过程),双方必须在同一个 16ms 帧预算内完成布局与绘制。如果原生视图的尺寸需要动态协商,两套布局引擎会在同一帧内各自测量一次,布局耗时翻倍。

三类互操作问题都可以归到同一条同步契约上,下图把问题与修复手段对应起来:

flowchart LR A[&#34;Compose 保留模式<br/>LayoutNode 场景图&#34;] -->|&#34;同一 16ms 帧预算&#34;| B[&#34;原生即时布局<br/>Auto Layout / measure-layout&#34;] B --> C{&#34;同步契约是否显式实现&#34;} C -->|&#34;否&#34;| D[&#34;双重布局<br/>帧内两次测量&#34;] C -->|&#34;否&#34;| E[&#34;手势与焦点竞争<br/>父级滚动被吞&#34;] C -->|&#34;否&#34;| F[&#34;不稳定 lambda<br/>原生视图重建&#34;] D --> G[&#34;固定边界<br/>Modifier.fillMaxSize()&#34;] E --> H[&#34;requestDisallowInterceptTouchEvent<br/>shouldRecognizeSimultaneouslyWith<br/>焦点双向桥接&#34;] F --> I[&#34;remember 稳定 lambda<br/>update 幂等&#34;]

2.2 问题一:双重布局与帧预算

修复方式是给原生视图固定、稳定的边界,用显式约束替代内容协商:

kotlin 复制代码
AndroidView(
    factory = { context -> NativeMapView(context) },
    modifier = Modifier.fillMaxSize(), // 显式约束,避免 wrapContentSize
    update = { mapView -> mapView.setZoom(12f) },
)

Modifier.size() 或 fillMaxSize() 让原生布局引擎不再需要在帧内协商尺寸。

2.3 问题二:手势与焦点的双向同步

Android 上 AndroidView 的触摸事件先经过 ViewGroup 的命中测试,再到达 Compose 的手势检测器;iOS 上 UIKitView 的 UIGestureRecognizer 与 Compose 的指针输入系统存在竞争。不处理时,父级滚动容器会静默失去滚动手势。

Android 侧的最小防护:

kotlin 复制代码
AndroidView(
    factory = { context ->
        NativeMapView(context).apply {
            setOnTouchListener { v, event ->
                v.parent.requestDisallowInterceptTouchEvent(
                    event.action != MotionEvent.ACTION_UP
                )
                false
            }
        }
    },
    modifier = Modifier.fillMaxSize(),
)

iOS 侧需要协调 shouldRecognizeSimultaneouslyWith,否则滚动手势会被原生视图完全吞掉。

焦点是更隐蔽的失效点。Compose 的 FocusManager 和 iOS 原生焦点系统是两台独立的状态机。用户点击嵌入的 UIKitView 文本框时,Compose 的 onFocusChanged 不会触发,键盘避让逻辑失效。必须显式建立双向桥:

kotlin 复制代码
// iOS 侧,通过 expect/actual 桥接
UIKitView(
    factory = {
        NativeTextField().apply {
            onFocusGained = { focusRequester.requestFocus() }
            onFocusLost = { focusManager.clearFocus() }
        }
    },
)

2.4 问题三:不稳定 lambda 导致原生视图重建

传给 AndroidView / UIKitView 的 factory、update 或回调 lambda,如果捕获了每次重组都会新建的闭包,Compose 会在每次重组时销毁并重建原生视图。对地图、相机预览或广告 SDK,这意味着持续的初始化开销和视觉闪烁。修复方式是用 remember 稳定 lambda 引用,并确保 update 中的操作幂等。

三、性能:先归因到阶段,再决定优化还是回退

3.1 TechnoAlpin 案例的完整归因链

TechnoAlpin 的 iOS 主应用基于 SwiftUI,三个功能用 CMP 实现,其中一个 Canvas 密集的机房控制图在旋转动画时帧率掉到个位数。这个案例的价值在于展示了一条从误判到精确归因的排查路径。

第一步:排除组合阶段。 第一直觉是"过度重组",但 profiler 显示重组次数为零------瓶颈不在组合阶段。

第二步:定位到绘制阶段。 Canvas 内有数十个文本标签,引擎每秒尝试重新计算、光栅化并绘制这些文字 60 到 120 次。问题出在 Canvas 的 drawText 路径。

第三步:区分症状与根因。 团队用离屏位图缓存修复了症状:将静态文本一次性绘制到 ImageBitmap,帧率从个位数恢复到 120 Hz。但根因是 CMP 1.8.0 的 Skia 层回归:每当任何 layer 属性(包括 Modifier.graphicsLayer { rotationZ })变化,layer 会将记录的内容标记为无效,强制整个子树每帧重绘。修复落在 CMP 1.9.0(CMP-8491),在 1.9.0 上无需位图缓存即可流畅运行。

这个案例的方法论可以复用:先确定瓶颈在组合、布局还是绘制阶段,再判断是应用层问题、互操作问题还是框架层回归。 在绘制阶段打组合阶段的补丁(或反过来)只会浪费工时。iOS 上还有一类值得单独排查的绘制瓶颈:Metal 着色器编译在主线程上完成,新出现的混合模式、裁剪形状或渐变类型可能阻塞数毫秒,普通帧时间平均值会掩盖这类尖峰。

把这条归因链画成决策路径,避免在错误的阶段打补丁:

flowchart TD A[&#34;帧率异常&#34;] --> B{&#34;重组次数是否异常&#34;} B -->|&#34;是&#34;| C[&#34;组合阶段<br/>检查状态读取与 key&#34;] B -->|&#34;否,重组为零&#34;| D{&#34;布局耗时是否超预算&#34;} D -->|&#34;是&#34;| E[&#34;布局阶段<br/>检查双重测量与约束协商&#34;] D -->|&#34;否&#34;| F[&#34;绘制阶段<br/>Canvas / drawText / layer 失效&#34;] F --> G[&#34;应用层修复<br/>离屏位图缓存&#34;] G --> H{&#34;是否仍劣于原生基线&#34;} H -->|&#34;是&#34;| I[&#34;框架层回归<br/>CMP-8491,升级 1.9.0&#34;] H -->|&#34;否&#34;| J[&#34;验收通过&#34;]

3.2 基线与验收

性能阈值必须来自同设备、同场景的原生实现:

指标 测量方式 基线要求 越界动作
启动耗时 Android Macrobenchmark / iOS Instruments App Launch,各测 10 次取中位数 同设备原生冷启动 CMP 屏幕移出首屏;延迟初始化共享模块
滚动帧率 JankStats / Instruments Core Animation,采集 P99 帧耗时与掉帧率 同设备原生长列表 为 LazyColumn 的 items() 提供稳定 key;仍不达标则退回原生列表
包体积增量 对比引入前后同 ABI 产物 引入前同 ABI 体积 拆分共享模块;限定 CMP 页面范围
Canvas / 动画 单独采样帧率、首帧耗时、离屏缓存命中 原生实现下该动画帧率 离屏缓存;仍不达标退回原生绘制

LazyColumn 的稳定 key 是一个具体且高频的修复点:没有 key 时,Compose 使用位置索引作为身份,插入一项会导致后续所有行重组;使用业务 ID 而非 index 或随机值,才能让 Compose 正确 diff 列表变化。

3.3 Web 端不能复用移动端基线

Web 端必须单独建基线。运行时(Wasm 字节码即时编译 vs 原生机器码)、渲染后端(ComposeViewport 到 Canvas 的桥接 vs 平台合成器)、加载模型(每次冷启动下载并实例化 Wasm vs 安装包一次性落地)都不同。至少测三项:.wasm 压缩前后传输体积、首屏加载时间(拆成下载、编译、实例化、首帧渲染四段)、平均 FPS。移动端基线只用于判断"这个页面是否值得放到 Web",不能给 Web 端定阈值。

四、宿主接入与降级:让回退成为可验证的路径

4.1 Android:setContent / ComposeView 作为唯一接缝

kotlin 复制代码
class SettingsActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            SettingsScreen(
                state = uiState,
                onToggleNotifications = { holder.toggleNotifications() },
                onOpenNativePage = {
                    startActivity(Intent(this, NativeSettingsActivity::class.java))
                },
                onLogout = { finish() },
            )
        }
    }
}

存量 View 体系里嵌入时用 ComposeView,迁移粒度可以细到"一个页面里的一块区域"。

4.2 iOS:ComposeUIViewController + UIViewControllerRepresentable

先在 iosMain 包装:

kotlin 复制代码
fun SettingsScreenViewController(
    state: SettingsUiState,
    onOpenNativePage: () -> Unit,
    onLogout: () -> Unit,
): UIViewController = ComposeUIViewController {
    SettingsScreen(state, onOpenNativePage = onOpenNativePage, onLogout = onLogout)
}

Swift 侧用 UIViewControllerRepresentable 接入,makeUIViewController 返回上面的控制器。构建配置中,embedAndSignAppleFrameworkForXcode 任务将 Kotlin 编译为 framework 并签名,Framework Search Paths 指向 $(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)。三个 iOS target(iosArm64、iosSimulatorArm64、iosX64)都要声明,iosMain 依赖 compose.ui;使用该脚本时不要在 Embed Frameworks 里重复嵌入,否则签名冲突。

4.3 降级:宿主级开关与单页回退

宿主级开关。 特性开关放在共享层,双端读同一个值:

kotlin 复制代码
object SettingsFeatureFlag {
    var useComposeUi: Boolean = true
}

Android 侧 useComposeUi = false 时直接 setContentView(R.layout.activity_settings_native);iOS 侧切换 SettingsScreenView 与 NativeSettingsView。关键约束:原生兜底实现不 import shared,不引用任何 Compose 类型。

单页回退。 局部嵌入的页面,回退范围限制在单页或单功能。ComposeView 换回原 XML,UIViewControllerRepresentable 换回纯 SwiftUI 视图,共享模块从该页面的渲染路径中退出。

验证清单(按顺序,任一不通过先解决再推进)。

步骤 操作 预期结果 不通过时查什么
1 ./gradlew :shared:linkDebugFrameworkIosSimulatorArm64 产出 framework,头文件可见宿主函数 baseName、target 声明、compose.ui 依赖
2 Xcode import shared 无 No such module Framework Search Paths、embed 脚本是否执行
3 Android 打开共享屏幕 界面渲染,状态切换生效 setContent 是否在生命周期内调用
4 iOS 打开共享屏幕 同一份界面,两个回调被触发 makeUIViewController 是否返回共享控制器
5 双端换回原生兜底 仍可编译运行 原生兜底是否误引用共享类型

第 5 步是分水岭。降级路径只有被真正走过,才算存在。

宿主接入与降级路径的整体结构如下------开关与单页回退是两条独立的退出通道:

flowchart TD A[&#34;共享层 SettingsScreen&#34;] --> B{&#34;SettingsFeatureFlag.useComposeUi&#34;} B -->|&#34;true&#34;| C[&#34;Android: setContent / ComposeView&#34;] B -->|&#34;true&#34;| D[&#34;iOS: ComposeUIViewController<br/>+ UIViewControllerRepresentable&#34;] B -->|&#34;false&#34;| E[&#34;宿主级开关回退<br/>setContentView / NativeSettingsView&#34;] C --> F[&#34;单页回退<br/>ComposeView 换回原 XML&#34;] D --> G[&#34;单页回退<br/>换回纯 SwiftUI 视图&#34;] E --> H[&#34;原生兜底<br/>不 import shared&#34;] F --> H G --> H

4.4 停止优化的判断线

以下任一条件成立时,继续在 CMP 侧调优的经济性已经不成立:

  • 完成阶段归因和常规优化(稳定 key、离屏缓存、焦点桥接、lambda 稳定化)后,仍劣于原生基线 15% 以上;
  • 页面主体是强平台耦合交互(视频编辑、复杂地图手势、自绘图表),CMP 侧只能靠大面积原生插槽拼装;
  • 无障碍或系统行为在显式桥接后仍不达标,且属于高频关键路径;
  • 异常堆栈落在 Skia 渲染层或 Kotlin/Native 桥接层,短期无法定位。

五、CMP 不适用的场景

强平台耦合的交互。 相机预览、地图手势、视频编辑、复杂自绘图表,CMP 侧要么依赖大面积原生插槽,要么体验有折损。顺应渲染管线边界(如用 SurfaceView 处理相机)比强行统一更合理。

对首屏启动极度敏感的路径。 如果共享模块初始化进入首屏关键路径,可能带来可感知延迟。应把 CMP 屏幕移出首屏,或延迟初始化重型依赖。

UI 栈深度交织、无法隔离的存量应用。 SharedKernel 模式的前提是模块可切割;切不开,迁移就会变成全局重写。

团队缺少 Compose 与 KMP 工程经验。 CMP 要求理解声明式 UI 的重组语义、平台源集的编译产物、互操作的同步契约。没有这些基础,局部嵌入或共享层驱动是更现实的起点。

六、结论

CMP 稳定版解决了"能不能用于生产",但没有替团队回答"共享多深、边界在哪、性能怎么验收、回退是否真实可用"。四个可验证的判断,可作为决策依据:

第一,commonMain 是否能在 iOS 和 Wasm 上零修改编译通过。 不能通过,说明平台 API 泄漏进了共享层,边界在源头就是错的。

第二,互操作的同步契约是否被显式实现。 原生视图是否有固定边界、手势是否双向转发、焦点是否桥接、lambda 是否稳定------这四项是互操作正常工作的前提,不是可选的"优化项"。

第三,性能问题是否能被归因到组合、布局或绘制阶段。 不能归因,就不知道是在修应用层问题还是在修框架层回归。TechnoAlpin 的教训是:重组次数为零而帧率个位数,问题在绘制,不在组合。

第四,原生兜底是否独立于共享模块。 兜底实现 import shared 的那一刻,回退路径就名存实亡。

四项都成立,才谈得上向更深的共享扩展;任何一项不成立,就应该停在当前深度,或者退回原生。CMP 允许你在"一个组件"到"整个 UI"之间选择深度------而选择深度的依据,是上述可验证的工程条件,不是别人用了多深。

相关推荐
booboosui1 小时前
安卓唤醒词:12 维 MFCC 加 DTW,阈值 1.8
android
事圆则缓1 小时前
从开机到首帧:Android 系统与应用启动流程源码分析
android
AI吃大瓜2 小时前
指针压力表读数4:Android实现指针压力表读数(可实时检测)
android·指针读数·指针压力表·压力表·压力表读数
fengci.2 小时前
CTF2刷题记录(困难部分)
android·开发语言·前端·学习·php
hunterandroid2 小时前
[Android 从零到一] ARouter 路由框架原理:APT 生成路由表与组件化通信
android
最好一梦2 小时前
佳维视安卓工控机在堆垛机中的应用
android
传奇开心果编程3 小时前
组合优先开发范式:从汉字造字原理到声明式UI组件组合思想
android·flutter·ios·swiftui·移动开发·harmonyos·android jetpack
腾讯端服务4 小时前
一码Duo端-Kuikly-iOS折叠屏新UI适配实践
android·ui·ios·kotlin·鸿蒙
T量子永生4 小时前
Android SISO 分布式编译集群配置方案
android·分布式·云计算·源代码管理