【Compose Multiplatform 跨端开发学与练】第7课 平台适配与互操作

本课目标:理解 expect/actual 的编译时契约本质,掌握接口注入作为更优替代方案的判断标准,学会 Compose 与 SwiftUI 的双向互操作技术,建立"平台差异分层决策"的系统方法论。

系列整体规划

课次 主题 核心内容 难度
第1课 从零开始 技术概览、环境搭建、第一个应用、代码解读 ⭐
第2课 Compose 基础语法 @Composable、状态管理、重组机制、Modifier 体系 ⭐⭐
第3课 布局与组件 Column/Row/Box、LazyColumn、Material3 组件库 ⭐⭐
第4课 导航与路由 Navigation Compose、类型安全路由、深层链接 ⭐⭐⭐
第5课 网络与数据层 Ktor 客户端、序列化、Repository 模式 ⭐⭐⭐
第6课 状态管理与架构 ViewModel、单向数据流、依赖注入 ⭐⭐⭐⭐
第7课 平台适配与互操作 expect/actual、SwiftUI 互操作、平台特定 API ⭐⭐⭐⭐
第8课 资源管理与主题 多平台资源、图片加载、深浅色主题 ⭐⭐⭐
第9课 测试与调试 Compose UI 测试、单元测试、性能分析 ⭐⭐⭐⭐
第10课 发布与部署 Android/iOS/桌面/Web 打包发布、CI/CD ⭐⭐⭐⭐⭐

第7课 平台适配与互操作

一、expect/actual:编译时契约机制

1.1 从"运行时分支"到"编译时契约"

如果你有前端开发经验,一定熟悉条件编译的痛点。在 JavaScript 中,平台差异通常用运行时判断来处理:

javascript 复制代码
// 运行时分支:类型系统无法验证完整性
function getUUID() {
  if (Platform.OS === 'web') {
    return crypto.randomUUID();
  } else {
    return require('uuid').v4();
  }
}

这种方式的根本问题是:类型系统无法验证每个平台的实现是否齐全。 如果你遗漏了某个平台的分支,编译不会报错,只有到了运行时才会暴露问题。

Kotlin Multiplatform 的 expect/actual 给出了一个完全不同的答案:编译时强制实现。

kotlin 复制代码
// commonMain:声明契约,不包含实现
expect fun generateUUID(): String

// androidMain:提供平台实现
actual fun generateUUID(): String = java.util.UUID.randomUUID().toString()

// iosMain:提供平台实现
actual fun generateUUID(): String = platform.Foundation.NSUUID().UUIDString()

关键差异在于:如果你在 commonMain 写了 expect,但某个平台忘记写 actual,编译器直接报错。 这不是运行时的防御性检查,而是编译期的硬性约束。

1.2 三种声明形式

expect/actual 支持多种声明形式,选择取决于使用场景:

函数级别(最推荐) 粒度最细,最容易测试和维护:

kotlin 复制代码
// commonMain
expect fun getPlatformName(): String

// androidMain
actual fun getPlatformName(): String = "Android"

// iosMain
actual fun getPlatformName(): String = "iOS"

对象级别 适合需要单例的场景,比如日志工具:

kotlin 复制代码
// commonMain
expect object AppLogger {
    fun d(message: String)
    fun e(message: String, throwable: Throwable? = null)
}

// androidMain
actual object AppLogger {
    actual fun d(message: String) { Log.d("App", message) }
    actual fun e(message: String, throwable: Throwable?) { /* ... */ }
}

类级别 只在特殊场景下使用,比如需要继承平台已有的基类时。但 Kotlin 官方明确指出:expect/actual 类处于 Beta 状态,且在简单场景下不推荐使用。

1.3 接口注入:比 expect/actual 更好的选择

Kotlin 官方文档给出了一个容易被忽视的建议:在大多数情况下,应该优先使用普通的 Kotlin 接口,而不是 expect/actual。

kotlin 复制代码
// commonMain:定义接口
interface Platform {
    val name: String
}

// androidMain:提供实现
class AndroidPlatform : Platform {
    override val name: String = "Android ${Build.VERSION.SDK_INT}"
}

// iosMain:提供实现
class IOSPlatform : Platform {
    override val name: String = "iOS ${UIDevice.currentDevice.systemVersion}"
}

为什么接口优于 expect/actual 类?接口不限制每个平台只能有一个实现。 你可以在测试中注入假实现,可以在同一平台上提供多个实现,可以更容易地做依赖注入。

判断标准 :如果平台差异是一组行为 (函数、属性),用 expect/actual 函数。如果平台差异是一组能力(接口的实现),用接口 + 依赖注入。Koin 官方文档同样建议:对于需要测试的业务逻辑,优先使用接口而非 expect 类。

1.4 WASM 平台的特殊处理

在 Web(Wasm)平台上,访问 JavaScript API 需要特殊声明:

kotlin 复制代码
// wasmJsMain
@JsFun("() => 'WASM (Web)'")
external fun getWasmPlatformName(): String

actual fun getPlatformName(): String = getWasmPlatformName()

WASM 平台不支持 js() 内联函数,需要通过 external 声明 + @JsFun 注解来调用 JS 原生 API。

1.5 状态栏适配:expect/actual 的 Composable 应用

平台特定的 UI 行为同样可以用 expect/actual 处理。比如状态栏样式:

kotlin 复制代码
// commonMain
@Composable
expect fun PlatformStatusBar(darkIcons: Boolean)

// androidMain
@Composable
actual fun PlatformStatusBar(darkIcons: Boolean) {
    val systemUiController = rememberSystemUiController()
    SideEffect {
        systemUiController.setStatusBarColor(Color.Transparent, darkIcons)
    }
}

Android 侧需要将平台副作用放在 SideEffect {} 中,确保只在组合提交后执行,避免重组期间触发。iOS 侧通常依赖 UIKit 互操作或 Info.plist 配置,不需要在 Compose 层强行模拟。

二、SwiftUI 互操作

2.1 双向嵌入的整体架构

CMP 与 SwiftUI 的互操作是双向的:你可以把 Compose 嵌入 SwiftUI 应用,也可以把 SwiftUI 嵌入 Compose 界面。 这为渐进式迁移和混合开发提供了基础。

理解这个架构的关键是:Compose 和 SwiftUI 之间的桥梁是一个 UIViewController。 Compose 侧的 ComposeUIViewController 产出 UIViewController,SwiftUI 侧用 UIViewControllerRepresentable 包装它。反过来,SwiftUI 的视图被 UIHostingController 包装成 UIViewController,传递给 Compose 侧的 UIKitViewController。

2.2 Compose 嵌入 SwiftUI

在 SwiftUI 应用中使用 Compose,Kotlin 侧创建一个返回 UIViewController 的函数:

kotlin 复制代码
// iosMain
fun MainViewController(): UIViewController = ComposeUIViewController {
    MaterialTheme {
        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            Text("This is Compose code", fontSize = 20.sp)
        }
    }
}

Swift 侧用 UIViewControllerRepresentable 包装:

swift 复制代码
import SwiftUI
import ComposeApp

struct ComposeView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        return Main_iosKt.MainViewController()
    }
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

struct ContentView: View {
    var body: some View {
        VStack {
            Text("SwiftUI Header")
            ComposeView().frame(height: 300)
        }
    }
}

一个必须注意的配置 :CMP 渲染需要显式启用高刷新率。在 iOS 应用的 Info.plist 中添加 CADisableMinimumFrameDurationOnPhone 键,否则应用会在运行时崩溃。

2.3 SwiftUI 嵌入 Compose:工厂模式

反向嵌入需要 Kotlin 侧接受一个 () -> UIViewController 的工厂函数。JetBrains 官方示例推荐使用 CompositionLocal + 工厂接口 的模式,让共享代码声明意图,平台侧提供实现。

Kotlin 侧定义工厂接口和 CompositionLocal:

kotlin 复制代码
// commonMain(或 iosMain)
interface NativeViewFactory {
    fun createPaymentButton(onPaid: () -> Unit): UIViewController
}

val LocalNativeViewFactory = staticCompositionLocalOf<NativeViewFactory> {
    error("NativeViewFactory not provided")
}

// iOS 入口提供工厂
fun MainViewController(factory: NativeViewFactory): UIViewController =
    ComposeUIViewController {
        CompositionLocalProvider(LocalNativeViewFactory provides factory) {
            App()
        }
    }

共享 UI 中声明原生组件的位置:

kotlin 复制代码
@Composable
fun PaymentScreen() {
    val factory = LocalNativeViewFactory.current
    Column {
        Text("选择支付方式")
        UIKitViewController(
            factory = { factory.createPaymentButton(onPaid = { /* ... */ }) },
            modifier = Modifier.fillMaxWidth().height(56.dp)
        )
    }
}

Swift 侧实现工厂:

swift 复制代码
class IOSNativeViewFactory: NativeViewFactory {
    func createPaymentButton(onPaid: @escaping () -> Void) -> UIViewController {
        let button = PaymentButton(onPaid: onPaid)
        return UIHostingController(rootView: button)
    }
}

这个模式的优雅之处在于:共享代码(commonMain)完全不知道 UIKit 的存在,它只声明"这里需要一个支付按钮"。平台实现通过依赖注入提供。

2.4 典型应用场景

SwiftUI 嵌入 Compose 的典型场景是使用平台专属的 UI 组件。以下是一些真实场景:

  • 地图:Apple MapKit,不是 Web 嵌入
  • WebView:WKWebView,真正的原生网页渲染
  • 相机:AVCaptureSession
  • 支付:Stripe 的支付表单,只有 UIKit/SwiftUI 版本,无法用 Compose 重新实现

以地图为例:

swift 复制代码
Main_iosKt.ComposeEntryPointWithUIViewController(
    createUIViewController: {
        let region = Binding.constant(
            MKCoordinateRegion(
                center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
                span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
            )
        )
        let mapView = Map(coordinateRegion: region)
        return UIHostingController(rootView: mapView)
    }
)

2.5 视频播放器的互操作策略选择

KINTO 技术博客分享了一个真实案例:在 CMP 中实现视频播放器时,需要选择SurfaceView (零拷贝、支持 DRM)还是TextureView(支持圆角裁剪和变换)。

他们的结论是:选择权本身就是价值。 在同一个应用中,角圆预览卡使用 TextureView(为了保持设计的圆角不被 Square 视频突破),全屏播放使用 SurfaceView(为了性能和 DRM 支持)。

这个案例的启示是:平台互操作不只是"能用",还要考虑视觉保真度与性能的权衡。CMP 的互操作 API 提供了这种细粒度控制的能力。

2.6 原生文本输入:iOS 体验的关键拼图

CMP 1.11.0 引入了基于 UIView 的原生文本输入实现(实验性)。这听起来像是一个小改进,但对 iOS 应用的实际体验影响巨大。

原生文本输入带来的改进包括:精确的光标移动、原生手势和选择手柄、系统上下文菜单(包括自动填充、翻译、搜索)。

为什么这很重要?文本输入是手机上最常被触摸的交互。 如果跨平台工具包在这件事上"露怯",用户不会报 bug 说"缺少放大镜",他们只是感觉应用"不对劲",然后降低对应用的信任。自动填充尤其关键:如果登录或结账表单不能调出保存的凭证或短信验证码,转化率会受影响。

如果你想在 iOS 上获得最原生的体验,可以在特性分支上测试这个功能,在登录、搜索、表单等输入密集的屏幕上启用它。原有的跨平台文本输入仍然是稳定的默认选项。

2.7 混合导航架构

在真实的混合应用中,导航需要跨越 Compose 和 SwiftUI 两个世界。一个实用的模式是路由常量 + 平台桥接:

kotlin 复制代码
// shared:定义跨平台路由
object Routes {
    const val LOGIN = "login"
    const val SCREEN_A = "screen_a"
    const val SETTINGS = "settings"
}

// iosMain:根据初始路由决定显示哪个 Compose 页面
fun createComposeViewController(initialRoute: String): UIViewController {
    return ComposeUIViewController {
        when (initialRoute) {
            Routes.LOGIN -> LoginScreen()
            Routes.SCREEN_A -> ScreenA()
            else -> DefaultScreen()
        }
    }
}

SwiftUI 侧用 NavigationStack 管理导航路径,将 Compose 的 UIViewController 作为目的地嵌入。这种模式让原生导航效果(如 iOS 的返回手势和转场动画)和 Compose 内容可以共存。

三、平台差异的分层决策框架

经过本课的学习,你可以建立一个清晰的决策框架:

第一层:多平台库优先。 如果 Ktor、SQLDelight、Koin 这些库已经提供了跨平台实现,直接使用,不需要平台适配。

第二层:接口 + DI。 如果平台差异是一组能力(需要注入不同的实现),用普通接口定义契约,通过 DI 容器注入平台实现。

第三层:expect/actual 函数。 如果平台差异是一组行为,且每个平台只需要一个实现,用 expect/actual 函数。这是最轻量的方案。

第四层:expect/actual 类(谨慎使用)。 只在需要继承平台基类时使用,且要接受 Beta 状态的风险。

第五层:原生 UI 互操作。 如果平台差异涉及UI 组件(地图、相机、支付表单),用 UIKitView/UIKitViewController(iOS)或 AndroidView(Android)嵌入原生视图。

一个实用原则 :尽可能多的代码放在 commonMain,只在必要时使用 expect/actual 在平台特定源集中实现差异。

四、习题与参考答案

本课习题分为三类:概念理解 (1-5 题)、代码实践 (6-11 题)、综合设计(12-15 题)。

概念理解

习题 1:expect/actual 与运行时分支的区别

题目:前端开发中常用运行时条件判断处理平台差异,Kotlin 的 expect/actual 有什么本质不同?

参考答案:运行时分支在打包或运行时判断,类型系统无法验证完整性,遗漏平台实现要等到运行时才发现。expect/actual 是编译时契约,编译器强制每个目标平台提供 actual 实现,遗漏会直接编译失败。

习题 2:接口 vs expect/actual 类

题目:Kotlin 官方建议优先使用接口而非 expect/actual 类,为什么?

参考答案:接口不限制每个平台只能有一个实现,测试时可以注入假实现,同一平台可以有多套实现。expect/actual 类处于 Beta 状态,且把设计限制为"每个平台一个实现",灵活性更低。

习题 3:Compose 与 SwiftUI 的桥梁

题目:Compose 和 SwiftUI 之间的桥梁是什么?为什么用这个类型作为桥梁?

参考答案 :桥梁是 UIViewController。Compose 的 ComposeUIViewController 产出 UIViewController,SwiftUI 用 UIViewControllerRepresentable 包装它。反过来,SwiftUI 视图用 UIHostingController 包装成 UIViewController 传给 Compose。UIViewController 是 iOS 上最通用的视图容器类型,两个框架都能与之互操作。

习题 4:为什么 SwiftUI 不能直接在 Kotlin 中编写

题目:为什么在 Compose 中嵌入 SwiftUI 必须通过工厂函数传递,而不能直接在 Kotlin 中写 SwiftUI 代码?

参考答案 :Kotlin/Native 只能调用 Swift/Objective-C 的编译产物,不能"生成"Swift 代码。SwiftUI 的声明式语法是 Swift 编译器的特性,Kotlin 编译器无法解析。所以必须在 Swift 中写好 SwiftUI 视图,包装成 UIViewController,再作为工厂函数传递给 Kotlin。

习题 5:原生文本输入为什么重要

题目:CMP 1.11.0 的原生文本输入为什么被视为 iOS 体验的关键改进?

参考答案:文本输入是手机上最常被触摸的交互。跨平台工具包如果在这件事上表现不佳,用户不会报 bug,而是感觉应用"不对劲"并降低信任。自动填充尤其关键:如果登录或结账表单不能调出保存的凭证,转化率会受影响。基于 UIView 的原生实现让 Compose 应用继承了系统行为,而不是重新实现一个"差不多"的版本。

代码实践

习题 6:实现平台名称获取

题目 :用 expect/actual 实现一个 getPlatformName(): String 函数,Android 返回 "Android",iOS 返回 "iOS",桌面端返回 "Desktop",Web 返回 "Web"。

参考答案:

kotlin 复制代码
// commonMain
expect fun getPlatformName(): String

// androidMain
actual fun getPlatformName(): String = "Android"

// iosMain
actual fun getPlatformName(): String = "iOS"

// desktopMain
actual fun getPlatformName(): String = "Desktop"

// wasmJsMain
@JsFun("() => 'Web'")
external fun getWebPlatformName(): String
actual fun getPlatformName(): String = getWebPlatformName()
习题 7:用接口替代 expect/actual 类

题目:把习题 6 的 expect/actual 函数改为接口 + 工厂函数的方式。

参考答案:

kotlin 复制代码
// commonMain
interface Platform { val name: String }
expect fun createPlatform(): Platform

// androidMain
class AndroidPlatform : Platform {
    override val name: String = "Android"
}
actual fun createPlatform(): Platform = AndroidPlatform()

// iosMain
class IOSPlatform : Platform {
    override val name: String = "iOS"
}
actual fun createPlatform(): Platform = IOSPlatform()

延伸思考 :这种模式比直接 expect/actual 类更灵活。你可以在测试中创建一个 TestPlatform,在 DI 容器中替换实现,而不需要为每个平台提供 actual 类。

习题 8:统一日志接口

题目 :用 expect/actual 对象实现一个 AppLogger,Android 用 android.util.Log,iOS 用 NSLog,桌面端用 println。

参考答案:

kotlin 复制代码
// commonMain
expect object AppLogger {
    fun d(message: String)
    fun e(message: String, throwable: Throwable? = null)
}

// androidMain
actual object AppLogger {
    actual fun d(message: String) { android.util.Log.d("App", message) }
    actual fun e(message: String, throwable: Throwable?) {
        android.util.Log.e("App", message, throwable)
    }
}

// iosMain
actual object AppLogger {
    actual fun d(message: String) { platform.Foundation.NSLog("DEBUG: $message") }
    actual fun e(message: String, throwable: Throwable?) {
        platform.Foundation.NSLog("ERROR: $message, cause: $throwable")
    }
}

// desktopMain
actual object AppLogger {
    actual fun d(message: String) { println("DEBUG: $message") }
    actual fun e(message: String, throwable: Throwable?) { println("ERROR: $message, $throwable") }
}
习题 9:生成 UUID

题目:实现跨平台 UUID 生成函数。

参考答案:

kotlin 复制代码
// commonMain
expect fun randomUUID(): String

// androidMain
actual fun randomUUID(): String = java.util.UUID.randomUUID().toString()

// iosMain
actual fun randomUUID(): String = platform.Foundation.NSUUID().UUIDString()

// desktopMain
actual fun randomUUID(): String = java.util.UUID.randomUUID().toString()
习题 10:Compose 嵌入 SwiftUI 的 Kotlin 侧(工厂模式)

题目 :用工厂模式实现 Kotlin 侧的入口函数,接受一个 NativeViewFactory 并在 Compose 中展示。

参考答案:

kotlin 复制代码
// commonMain
interface NativeViewFactory {
    fun createStarView(): UIViewController
}

val LocalNativeViewFactory = staticCompositionLocalOf<NativeViewFactory> {
    error("NativeViewFactory not provided")
}

// iosMain
fun MainViewController(factory: NativeViewFactory): UIViewController =
    ComposeUIViewController {
        CompositionLocalProvider(LocalNativeViewFactory provides factory) {
            Column(
                modifier = Modifier.fillMaxSize().padding(16.dp),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text("Compose 中的原生组件")
                val nativeFactory = LocalNativeViewFactory.current
                UIKitViewController(
                    factory = { nativeFactory.createStarView() },
                    modifier = Modifier.size(200.dp).border(1.dp, Color.Gray)
                )
            }
        }
    }
习题 11:Swift 侧实现工厂

题目 :编写 Swift 代码,实现习题 10 的 NativeViewFactory,创建一个包含星形图标的 SwiftUI 视图。

参考答案:

swift 复制代码
import SwiftUI
import ComposeApp

class IOSNativeViewFactory: NativeViewFactory {
    func createStarView() -> UIViewController {
        let view = VStack(spacing: 16) {
            Text("原生 SwiftUI 组件")
                .font(.headline)
            Image(systemName: "star.fill")
                .foregroundColor(.yellow)
                .font(.largeTitle)
        }
        return UIHostingController(rootView: view)
    }
}

综合设计

习题 12:带平台标识的首页

题目:实现一个首页,顶部显示平台名称,中间是共享的 Compose UI。用接口注入的方式提供平台名称。

参考答案:

kotlin 复制代码
// commonMain
interface Platform { val name: String }

@Composable
fun App(platform: Platform) {
    MaterialTheme {
        Column(
            modifier = Modifier.fillMaxSize().safeContentPadding(),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(
                text = "运行平台: ${platform.name}",
                style = MaterialTheme.typography.headlineSmall,
                modifier = Modifier.padding(24.dp)
            )
            Text("这是共享的 Compose UI")
        }
    }
}

// 各平台入口分别创建 Platform 实例并传入
// Android: App(AndroidPlatform())
// iOS: App(IOSPlatform())
// Desktop: App(DesktopPlatform())
习题 13:平台专属文件存储路径

题目 :用 expect/actual 实现一个 getCacheDirectory(): String,Android 返回 context.cacheDir.path,iOS 返回 NSCachesDirectory 路径,桌面端返回 System.getProperty("java.io.tmpdir")。

参考答案:

kotlin 复制代码
// commonMain
expect fun getCacheDirectory(): String

// androidMain
actual fun getCacheDirectory(): String {
    return androidContext.cacheDir.path
}

// iosMain
actual fun getCacheDirectory(): String {
    val paths = NSSearchPathForDirectoriesInDomains(
        NSCachesDirectory, NSUserDomainMask, true
    )
    return paths.first() as String
}

// desktopMain
actual fun getCacheDirectory(): String = System.getProperty("java.io.tmpdir")

延伸思考 :Android 的 Context 不能直接放在 androidMain 的顶层。通常通过 Application 单例或 DI 容器注入。Koin 文档推荐使用 ContextWrapper 模式 :在 commonMain 定义 interface AppContext,Android 侧用 AndroidAppContext(context) 实现,iOS 侧用空实现。

习题 14:混合导航中的路由分发

题目 :实现一个 createComposeViewController(route: String) 函数,根据路由字符串返回不同的 Compose 页面,供 SwiftUI 导航使用。

参考答案:

kotlin 复制代码
// iosMain
fun createComposeViewController(route: String): UIViewController {
    return ComposeUIViewController {
        when (route) {
            "login" -> LoginScreen()
            "profile" -> ProfileScreen()
            "settings" -> SettingsScreen()
            else -> Text("未知路由: $route")
        }
    }
}
习题 15:平台特定的分享功能

题目 :实现一个 ShareManager,Android 用 Intent.ACTION_SEND,iOS 用 UIActivityViewController,桌面端用剪贴板复制。在 Compose UI 中提供一个分享按钮。

参考答案:

kotlin 复制代码
// commonMain
interface ShareManager {
    fun share(text: String)
}

@Composable
expect fun rememberShareManager(): ShareManager

// androidMain
class AndroidShareManager(private val context: Context) : ShareManager {
    override fun share(text: String) {
        val intent = Intent(Intent.ACTION_SEND).apply {
            type = "text/plain"
            putExtra(Intent.EXTRA_TEXT, text)
        }
        context.startActivity(Intent.createChooser(intent, "分享"))
    }
}

@Composable
actual fun rememberShareManager(): ShareManager {
    val context = LocalContext.current
    return remember(context) { AndroidShareManager(context) }
}

// iosMain
class IosShareManager : ShareManager {
    override fun share(text: String) {
        val activityVC = UIActivityViewController(
            activityItems = listOf(text),
            applicationActivities = null
        )
        // 获取当前 UIViewController 并 present
    }
}

@Composable
actual fun rememberShareManager(): ShareManager = remember { IosShareManager() }

使用方式:

kotlin 复制代码
@Composable
fun ShareButton(content: String) {
    val shareManager = rememberShareManager()
    Button(onClick = { shareManager.share(content) }) {
        Text("分享")
    }
}

五、本课小结

expect/actual 的本质 :编译时契约机制。commonMain 声明 expect,各平台提供 actual,编译器强制全覆盖。与运行时分支相比,遗漏平台实现会在编译期暴露,而非运行时。

接口优于 expect/actual 类:接口不限制每个平台只有一个实现,测试时可注入假实现。expect/actual 类处于 Beta 状态,适合需要继承平台基类的特殊场景。

SwiftUI 双向互操作 :桥梁是 UIViewController。Compose 嵌入 SwiftUI 用 UIViewControllerRepresentable,SwiftUI 嵌入 Compose 用 UIKitViewController + UIHostingController。工厂模式(CompositionLocal + 接口)让共享代码声明意图,平台侧提供实现。

原生文本输入的重要性:CMP 1.11.0 的 UIView 原生文本输入(实验性)解决了 iOS 上最影响体验的交互细节------光标、手势、上下文菜单、自动填充。

分层决策框架:多平台库 > 接口 + DI > expect/actual 函数 > expect/actual 类 > 原生 UI 互操作。优先级从左到右递减,越靠左越简单、越可测试。

六、下一课预告

第8课 资源管理与主题

相关推荐
mmsx1 小时前
Android 地图数据链路:从 GDAL、KML 到多引擎适配
android·kotlin
工作10年+,存储芯片行业1 小时前
Linux NVMe 中断排查与性能优化:CPU 亲和性
linux·运维·服务器·windows·性能优化·ssd·pcie
2601_968900771 小时前
大模型版本回归评估实战:从能力指标到额度口径的完整链路
android·数据挖掘·回归
我命由我123451 小时前
Photoshop - Photoshop 使用对齐功能定位元素
学习·职场和发展·产品运营·求职招聘·职场发展·产品经理·学习方法
lpfasd1232 小时前
WinSW在Win7上失败真相-实测与修复
windows·nginx
阳光九叶草LXGZXJ3 小时前
达梦数据库-报错-16-MERGE INTO提示:无效的列名[XX]
linux·运维·数据库·sql·学习
xxwl5853 小时前
RabbitMQ 学习笔记
笔记·学习·rabbitmq
万山寒3 小时前
windows使用WinSW注册服务
windows
ao-weilai3 小时前
MySQL数据库:基本查询
android·数据库·mysql