Skip to content

第 8 章 平台通信 — Module / 调原生 API / expect-actual

学习目标:搞清楚 Kuikly 跨端代码"什么时候必须调原生";学会用 Module 调用平台能力;掌握 expect/actual 的进阶用法;理解 callKotlin / callNative 的双向通信模型。


8.1 为什么需要"调原生"

8.1.1 跨端 ≠ 全部跨端

Kuikly 解决了 UI 跨端,但有些能力 必须 调原生 API:

   ┌──────────────────────────────────────────────────────┐
   │  跨端能搞定的(90%):                                 │
   │     - UI 描述 / 布局                                  │
   │     - 业务逻辑 / 数据处理                              │
   │     - 网络请求 (Ktor/HttpModule)                      │
   │     - 路由跳转                                        │
   │                                                        │
   │  跨端搞不定的(10%,必须调原生):                       │
   │     - 调用相机 / 相册                                  │
   │     - 推送 (FCM / APNs / 鸿蒙推送)                    │
   │     - 设备信息 / 权限                                  │
   │     - 文件系统访问                                     │
   │     - 音视频 SDK                                       │
   │     - 第三方 SDK(支付、登录、IM)                     │
   └──────────────────────────────────────────────────────┘

📌 跨端框架的"60 分"是 UI,"100 分"靠平台通信能力。这一章就是教你怎么把那 10% 接进来。


8.2 两种通信方案:何时用哪个

   ┌────────────────────────────────────────────────────────────┐
   │  方案 A:expect / actual                                     │
   │  ─────────────────────────────                              │
   │  - KMP 原生方案                                              │
   │  - 静态绑定,编译期确定实现                                  │
   │  - 适合:纯函数式 API(getDeviceId / fileExists)             │
   ├────────────────────────────────────────────────────────────┤
   │  方案 B:Module(Kuikly 推荐)                               │
   │  ─────────────────────────────                              │
   │  - 框架封装的通信层                                          │
   │  - 动态注册,运行时按 name 查找                              │
   │  - 适合:复杂、有生命周期、有事件回调的能力                   │
   │     例:相机预览、IM 监听、定时器                             │
   └────────────────────────────────────────────────────────────┘

简单粗暴选型

  • 一行调用就完事 → expect/actual
  • 有事件、有回调、能力本身复杂 → Module

8.3 expect / actual 进阶:5 种常见模式

8.3.1 模式 1:纯函数

kotlin
// commonMain
expect fun getCurrentTimeMillis(): Long

// androidMain
actual fun getCurrentTimeMillis(): Long = System.currentTimeMillis()

// iosMain
import platform.Foundation.NSDate
actual fun getCurrentTimeMillis(): Long =
    (NSDate().timeIntervalSince1970 * 1000).toLong()

8.3.2 模式 2:expect class(带状态)

kotlin
// commonMain
expect class FileStorage(name: String) {
    fun put(key: String, value: String)
    fun get(key: String): String?
    fun remove(key: String)
}

// androidMain
actual class FileStorage actual constructor(name: String) {
    private val sp = appContext.getSharedPreferences(name, Context.MODE_PRIVATE)
    actual fun put(key: String, value: String) {
        sp.edit().putString(key, value).apply()
    }
    actual fun get(key: String): String? = sp.getString(key, null)
    actual fun remove(key: String) {
        sp.edit().remove(key).apply()
    }
}

// iosMain
import platform.Foundation.NSUserDefaults
actual class FileStorage actual constructor(private val name: String) {
    private val ud = NSUserDefaults(suiteName = name) ?: NSUserDefaults.standardUserDefaults
    actual fun put(key: String, value: String) = ud.setObject(value, forKey = key)
    actual fun get(key: String): String? = ud.stringForKey(key)
    actual fun remove(key: String) = ud.removeObjectForKey(key)
}

8.3.3 模式 3:expect interface

kotlin
// commonMain
expect interface Logger {
    fun debug(tag: String, msg: String)
    fun error(tag: String, msg: String, throwable: Throwable? = null)
}

8.3.4 模式 4:默认实现 + 可选覆写

kotlin
// commonMain
expect object DeviceInfo {
    val osName: String
    val osVersion: String
    fun isTablet(): Boolean
}

// androidMain
actual object DeviceInfo {
    actual val osName: String = "Android"
    actual val osVersion: String = Build.VERSION.RELEASE
    actual fun isTablet(): Boolean {
        val smallestWidth = appContext.resources.configuration.smallestScreenWidthDp
        return smallestWidth >= 600
    }
}

8.3.5 模式 5:异步 API(用 Kotlin 协程)

kotlin
// commonMain
expect suspend fun fetchLocation(): Pair<Double, Double>

// androidMain
actual suspend fun fetchLocation(): Pair<Double, Double> = suspendCancellableCoroutine { cont ->
    val client = LocationServices.getFusedLocationProviderClient(appContext)
    client.lastLocation
        .addOnSuccessListener { loc -> cont.resume(loc.latitude to loc.longitude) }
        .addOnFailureListener { e -> cont.resumeWithException(e) }
}

8.4 Module:Kuikly 的"动态调用桥"

8.4.1 Module 是什么

Module 是 Kuikly 设计的「统一的跨端能力调用接口」。所有平台能力都包装成 Module,业务代码通过 acquireModule<T>(name) 拿到实例,调用方法。

8.4.2 框架内置的 Module 清单

   ┌─────────────────────────────────────────────────────────┐
   │  Kuikly 内置 Module                                       │
   ├─────────────────────────────────────────────────────────┤
   │  HttpModule         网络请求                              │
   │  RouterModule       路由跳转                              │
   │  PageRouterModule   Pager 间跳转                          │
   │  StorageModule      KV 存储(SP / NSUserDefaults / LS)   │
   │  ToastModule        Toast 提示                            │
   │  TimerModule        定时器                                │
   │  ImagePreviewModule 大图预览                              │
   │  LogModule          日志输出                              │
   │  DeviceInfoModule   设备信息                              │
   │  NotificationModule 跨页面广播                            │
   │  CalendarModule     日历                                  │
   │  ...                                                      │
   └─────────────────────────────────────────────────────────┘

8.4.3 调用内置 Module

kotlin
class DemoPage : Pager() {

    override fun pageDidAppear() {
        super.pageDidAppear()

        // ① Toast
        acquireModule<ToastModule>(ToastModule.MODULE_NAME).showToast("欢迎光临")

        // ② 存储
        val storage = acquireModule<StorageModule>(StorageModule.MODULE_NAME)
        storage.setItem("user_name", "Tom")
        val name = storage.getItem("user_name")    // ★ 同步取

        // ③ 网络请求
        acquireModule<HttpModule>(HttpModule.MODULE_NAME)
            .request(
                url = "https://api.example.com/user",
                method = "GET",
                params = JSONObject(),
                callback = { result ->
                    val data = result.optJSONObject("data")
                    // 更新 UI
                }
            )

        // ④ 路由跳转
        acquireModule<PageRouterModule>(PageRouterModule.MODULE_NAME)
            .openPage("DetailPage", JSONObject().apply {
                put("itemId", "abc123")
            })

        // ⑤ 定时器
        val timer = acquireModule<TimerModule>(TimerModule.MODULE_NAME)
        timer.schedule(1000) { /* 1 秒后执行 */ }
    }
}

8.4.4 业务侧调 Module 的两种姿势

kotlin
// 姿势 1:每次都 acquire
acquireModule<ToastModule>(ToastModule.MODULE_NAME).showToast("hi")

// 姿势 2:缓存到字段
private val toast by lazy { acquireModule<ToastModule>(ToastModule.MODULE_NAME) }
toast.showToast("hi")

// 姿势 3:扩展函数(最常用,框架已封装)
toast("hi")     // 框架自带的扩展函数(不同版本可能有差异)

8.5 自定义 Module:调相机示例

8.5.1 步骤总览

   ┌──────────────────────────────────────────────────────┐
   │  自定义 Module 4 步法                                  │
   ├──────────────────────────────────────────────────────┤
   │  1. commonMain 定义 expect Module 接口                │
   │  2. 各 platformMain 写 actual 实现                    │
   │  3. 在 Application / App.swift 注册 Module            │
   │  4. 业务代码 acquireModule<CameraModule>(...) 调用    │
   └──────────────────────────────────────────────────────┘

8.5.2 步骤 1:commonMain 定义接口

kotlin
// shared/src/commonMain/kotlin/.../CameraModule.kt
package com.example.kuikly.modules

import com.tencent.kuikly.core.module.Module

expect class CameraModule : Module {
    /**
     * 调起系统相机拍照
     * @param onComplete 拍照完成回调,传回本地图片路径
     */
    fun takePhoto(onComplete: (String?) -> Unit)

    /**
     * 调起系统相册选图
     */
    fun pickFromAlbum(onComplete: (List<String>) -> Unit)

    companion object {
        const val MODULE_NAME = "Camera"
    }
}

8.5.3 步骤 2:Android 实现

kotlin
// shared/src/androidMain/kotlin/.../CameraModule.kt
package com.example.kuikly.modules

import android.content.Intent
import android.net.Uri
import android.provider.MediaStore
import com.tencent.kuikly.android.KuiklyActivity

actual class CameraModule : Module() {

    actual fun takePhoto(onComplete: (String?) -> Unit) {
        val activity = currentActivity as? KuiklyActivity ?: return onComplete(null)

        // 使用 Activity Result API 调起相机(Android 端真实代码会更复杂,这里简化)
        val tempFile = File(activity.cacheDir, "photo_${System.currentTimeMillis()}.jpg")
        val uri = FileProvider.getUriForFile(activity, "${activity.packageName}.fp", tempFile)

        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
            .putExtra(MediaStore.EXTRA_OUTPUT, uri)

        activity.launchForResult(intent) { resultCode, _ ->
            if (resultCode == Activity.RESULT_OK) {
                onComplete(tempFile.absolutePath)
            } else {
                onComplete(null)
            }
        }
    }

    actual fun pickFromAlbum(onComplete: (List<String>) -> Unit) {
        // 类似 takePhoto,调起 ACTION_PICK
        // ...
    }

    actual companion object {
        actual const val MODULE_NAME = "Camera"
    }
}

8.5.4 步骤 3:iOS 实现

kotlin
// shared/src/iosMain/kotlin/.../CameraModule.kt
package com.example.kuikly.modules

import platform.UIKit.UIImagePickerController
import platform.UIKit.UIImagePickerControllerSourceType.UIImagePickerControllerSourceTypeCamera

actual class CameraModule : Module() {

    actual fun takePhoto(onComplete: (String?) -> Unit) {
        val picker = UIImagePickerController()
        picker.sourceType = UIImagePickerControllerSourceTypeCamera
        picker.delegate = object : NSObject(), UIImagePickerControllerDelegateProtocol {
            override fun imagePickerController(
                picker: UIImagePickerController,
                didFinishPickingMediaWithInfo: Map<Any?, *>,
            ) {
                // 拿到 UIImage,写到本地,回调路径
                val path = saveUIImageToTemp(didFinishPickingMediaWithInfo)
                onComplete(path)
            }
        }

        currentViewController?.presentViewController(picker, animated = true, completion = null)
    }

    actual fun pickFromAlbum(onComplete: (List<String>) -> Unit) { /* ... */ }

    actual companion object {
        actual const val MODULE_NAME = "Camera"
    }
}

8.5.5 步骤 4:注册 Module

kotlin
// androidApp/.../KuiklyApplication.kt
class KuiklyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        ModuleRegistry.register(CameraModule.MODULE_NAME) { CameraModule() }
        ModuleRegistry.register(MyAuthModule.MODULE_NAME) { MyAuthModule() }
    }
}

8.5.6 业务调用

kotlin
@Page("PublishPage")
internal class PublishPage : Pager() {
    private var photoPath by observable<String?>(null)

    override fun body(): ViewBuilder {
        val ctx = this
        return {
            View {
                attr {
                    height(200f); allCenter()
                    backgroundColor(Color(0xFFE0E0E0L))
                    borderRadius(12f)
                    margin(16f)
                }
                event {
                    click {
                        acquireModule<CameraModule>(CameraModule.MODULE_NAME)
                            .takePhoto { path ->
                                ctx.photoPath = path
                            }
                    }
                }
                if (ctx.photoPath == null) {
                    Text { attr { text("📷 点击拍照"); fontSize(16f) } }
                } else {
                    Image {
                        attr { src(ctx.photoPath!!); flex(1f); resizeCover() }
                    }
                }
            }
        }
    }
}

8.6 双向通信:callKotlin & callNative

某些场景需要原生主动调 Kotlin 业务代码(比如 IM 收到消息推送)。Kuikly 提供 callKotlin / callNative

   ┌────────────────────────────────────────────────────┐
   │  callNative: Kotlin → 原生              (已讲)      │
   │     acquireModule<...>().method(...)                │
   ├────────────────────────────────────────────────────┤
   │  callKotlin: 原生 → Kotlin              (新内容)    │
   │     原生侧持有 KuiklyView 引用                       │
   │     调 view.callKotlin(method, args)                │
   │     Kuikly 派发到对应 Pager 的 onReceiveCall(...)    │
   └────────────────────────────────────────────────────┘

8.6.1 业务侧定义回调

kotlin
@Page("ChatPage")
internal class ChatPage : Pager() {

    private val messages = observableListOf<Message>()

    override fun created() {
        super.created()

        // 监听 IM 模块抛上来的"新消息"事件
        acquireModule<IMModule>(IMModule.MODULE_NAME)
            .onMessageReceived { msg ->
                messages.add(0, msg)
            }
    }
}

8.6.2 框架封装:跨页面广播

NotificationModule 是 Kuikly 自带的"事件总线",跨 Pager 通信首选:

kotlin
// 发送方(任意 Pager / 任意原生层)
acquireModule<NotificationModule>(NotificationModule.MODULE_NAME)
    .postNotification("UserLoggedOut", JSONObject().apply { put("reason", "manual") })

// 接收方
override fun created() {
    super.created()
    acquireModule<NotificationModule>(NotificationModule.MODULE_NAME)
        .addNotificationListener("UserLoggedOut") { params ->
            // 跳到登录页
        }
}

override fun pageWillDestroy() {
    super.pageWillDestroy()
    // ⚠️ 必须解绑,避免内存泄漏
    acquireModule<NotificationModule>(NotificationModule.MODULE_NAME)
        .removeNotificationListener("UserLoggedOut")
}

8.7 实战:3 个常用 Module 速学

8.7.1 HttpModule:发请求

kotlin
acquireModule<HttpModule>(HttpModule.MODULE_NAME).request(
    url = "https://api.example.com/articles",
    method = "GET",
    params = JSONObject().apply { put("page", 1) },
    headers = JSONObject().apply { put("Authorization", "Bearer xxx") },
    callback = { result ->
        // result 是 JSONObject
        val data = result.optJSONArray("data") ?: return@request
        val list = (0 until data.length()).map {
            val o = data.getJSONObject(it)
            Article(o.getString("id"), o.getString("title"))
        }
        articles.clear()
        articles.addAll(list)
    }
)

8.7.2 StorageModule:本地存储

kotlin
val storage = acquireModule<StorageModule>(StorageModule.MODULE_NAME)

storage.setItem("token", "abc123")
val token = storage.getItem("token")     // null 或 "abc123"
storage.removeItem("token")

// JSON
storage.setItem("user", JSONObject().apply {
    put("id", 1); put("name", "Tom")
}.toString())

8.7.3 TimerModule:定时器

kotlin
val timer = acquireModule<TimerModule>(TimerModule.MODULE_NAME)

// 一次性
timer.schedule(delay = 1000) {
    println("1 秒后")
}

// 周期
val taskId = timer.schedule(delay = 0, interval = 1000) {
    countdown--
    if (countdown <= 0) timer.cancel(taskId)
}

8.8 章末小结

                  ★ 第 8 章平台通信知识图谱 ★

       ┌──────────────────────┼─────────────────────┐
       │                      │                     │
   ┌───▼─────┐         ┌──────▼──────┐         ┌────▼─────┐
   │expect/actual│       │ Module      │         │双向通信  │
   ├─────────┤         ├─────────────┤         ├──────────┤
   │ 静态绑定│         │ 动态注册    │         │callKotlin│
   │ 编译期  │         │ 按 name 查找│         │callNative│
   │ 简单 API│         │ 复杂能力    │         │Notification│
   │ 函数 / 类│        │ 内置 N 个   │         │事件总线  │
   └─────────┘         └─────────────┘         └──────────┘

                  ┌───────────┴───────────┐
                  │                       │
              ┌───▼────┐             ┌────▼─────┐
              │ 框架内置 │             │ 自定义   │
              │ Http    │             │ 4 步法   │
              │ Storage │             │ expect→  │
              │ Toast   │             │ actual→  │
              │ Timer   │             │ register→│
              │ Router  │             │ acquire  │
              └─────────┘             └──────────┘

🎤 8.9 章末面试题(10 道高频题)

Q1. Kuikly 的"平台通信"有哪两种方案?怎么选?

  • expect / actual:KMP 原生方案,静态绑定,编译期确定实现。适合:纯函数式 API,一行能调完事
  • Module:Kuikly 推荐方案,动态注册,运行时按 name 查找。适合:复杂能力(生命周期、回调、事件),如相机、IM、推送

简单选型:一次性调用 → expect/actual;持续/双向交互 → Module

Q2. 为什么 Kuikly 不直接用反射做 Module 调用?

:因为 Kuikly 要跑在 Kotlin/Native 平台(iOS、鸿蒙),那边反射受限:

  • Kotlin/Native 不支持 KClass.declaredFunctions 这种完整反射
  • 即使支持,反射有性能开销

Kuikly 的 Module 注册用 (String) -> Module 工厂函数 + KSP 编译期生成代码,性能跟手写一样。

Q3. expect class 的构造函数怎么写?

:commonMain 声明的构造参数列表 = 所有 platformMain 必须实现的:

kotlin
// commonMain
expect class FileStorage(name: String) {
    fun put(key: String, value: String)
}

// androidMain
actual class FileStorage actual constructor(name: String) {
    private val sp = appContext.getSharedPreferences(name, Context.MODE_PRIVATE)
    actual fun put(key: String, value: String) { ... }
}

注意 actual constructor(...) 的写法。

Q4. acquireModule 是同步的还是异步的?

同步。Kuikly 在 App 启动时把所有 Module 注册到全局 ModuleRegistry,acquire 就是查 Map 拿实例,O(1) 同步操作。

但 Module 内部的方法可能是异步的(如 request 带 callback、takePhoto 带 onComplete)。这是因为这些能力本身就是异步的。

Q5. callKotlin 和 callNative 是什么?

:双向通信的两种方向:

  • callNative:Kotlin 主动调原生(用 acquireModule<...>().method(...)
  • callKotlin:原生主动调 Kotlin(一般通过 Module 内的 listener 或者 KuiklyView 的 callKotlin(method, args)

例如 IM 推送:原生层收到消息 → 主动调 Kuikly → Pager 收到 → 更新 UI。

Q6. NotificationModule 跟原生的"事件总线"是什么关系?

:NotificationModule 是 Kuikly 自带的「跨页面 / 跨 Native-Kotlin 事件广播」。它能让:

  • 任意 Pager 互相发消息
  • 原生层主动给 Pager 发消息
  • 一个事件多个订阅者

定位类似 Android 的 LocalBroadcastManager + EventBus。记得在 pageWillDestroy 里 removeListener,避免泄漏

Q7. 在 commonMain 里能不能 import 平台 API?

不能。commonMain 是平台无关的源码集,只能 import 跨端代码(Kotlin 标准库、Kuikly 框架、KMP 兼容库如 Ktor)。

如果业务需要平台 API,正确做法:

  1. 简单 API:用 expect/actual
  2. 复杂能力:定义 Module,platformMain 写 actual

千万别在 commonMain 里 import android.* —— 会编译失败。

Q8. Module 的 Companion.MODULE_NAME 为什么是字符串?

:因为 Module 注册是按 name 字符串查找的(动态注册表 + 工厂函数):

kotlin
ModuleRegistry.register("Camera") { CameraModule() }
val cam = acquireModule<CameraModule>("Camera")

字符串好处:

  • 跨语言通用(原生层、Kuikly 层都能用同一个字符串通信)
  • 支持动态加载(动态化场景下后下发的 Module 也能注册)
  • 解耦(业务代码不直接依赖 Module 类)

Q9. 拍照功能怎么从零实现到调通?

:4 步法:

  1. commonMain 定义 expect class CameraModule : Module { fun takePhoto(cb) }
  2. androidMain 写 actual 实现:调 MediaStore.ACTION_IMAGE_CAPTURE
  3. iosMain 写 actual 实现:用 UIImagePickerController
  4. Application 启动时 ModuleRegistry.register("Camera") { CameraModule() }
  5. 业务侧 acquireModule<CameraModule>(CameraModule.MODULE_NAME).takePhoto { path -> ... }

详细代码见 8.5 节。

Q10. 如果一个 Module 在 Android 实现了,iOS 没实现,会怎样?

编译失败。expect 必须在所有目标平台都有 actual 实现,否则 KMP 编译器会报:

error: expected class 'CameraModule' has no actual declaration in module 'iosMain'

应对方案

  • 不打算支持 iOS:可以在 iosMain 写一个空实现 + 抛 UnsupportedOperationException
  • 暂时不实现:先 stub 住,后续再补
  • 这能力 iOS 真没有:考虑改成跨端通用能力(如"拍照"换成"上传图片",iOS 走相册)

下一站 → 第 9 章 · 动态化 & 性能 →

🎬 可视化演示

演示加载缓慢或样式异常?点此在新标签页打开 ↗

💻 示例代码

kotlin
/**
 * 第 8 章配套代码 · 自定义 CameraModule(4 步法完整示例)
 *
 * 演示如何从 0 到 1 接入一个原生能力:
 *   ① commonMain 定义 expect class
 *   ② androidMain 写 actual 实现
 *   ③ iosMain 写 actual 实现
 *   ④ Application 注册 + 业务调用
 *
 * 真实工程会更复杂(权限、回调、错误处理),这里聚焦"通信骨架"。
 */

// ═══════════════════════════════════════════════════════════════════
// 步骤 ① commonMain:定义 expect class
// 文件位置:shared/src/commonMain/kotlin/com/example/kuikly/modules/CameraModule.kt
// ═══════════════════════════════════════════════════════════════════
package com.example.kuikly.modules

import com.tencent.kuikly.core.module.Module

expect class CameraModule() : Module {

    /**
     * 调起系统相机拍照
     * @param onComplete 拍照完成回调(path 为本地图片绝对路径,null = 用户取消或失败)
     */
    fun takePhoto(onComplete: (path: String?) -> Unit)

    /**
     * 从相册选图(多选)
     * @param maxCount 最多张数
     * @param onComplete 回调(选中的图片路径列表,空列表 = 取消)
     */
    fun pickFromAlbum(maxCount: Int = 1, onComplete: (paths: List<String>) -> Unit)

    /**
     * 检查相机权限
     */
    fun hasPermission(): Boolean

    companion object {
        const val MODULE_NAME = "Camera"
    }
}

// ═══════════════════════════════════════════════════════════════════
// 步骤 ② androidMain:Android 实现
// 文件位置:shared/src/androidMain/kotlin/com/example/kuikly/modules/CameraModule.kt
// ═══════════════════════════════════════════════════════════════════
/*
package com.example.kuikly.modules

import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.provider.MediaStore
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.example.kuikly.app.appContext
import com.tencent.kuikly.android.KuiklyActivity
import com.tencent.kuikly.core.module.Module
import java.io.File

actual class CameraModule actual constructor() : Module() {

    actual fun takePhoto(onComplete: (String?) -> Unit) {
        val activity = currentActivity() as? KuiklyActivity ?: run {
            onComplete(null); return
        }

        if (!hasPermission()) {
            requestCameraPermission(activity) { granted ->
                if (granted) takePhoto(onComplete) else onComplete(null)
            }
            return
        }

        val tempFile = File(activity.cacheDir, "kuikly_photo_${System.currentTimeMillis()}.jpg")
        val authority = "${activity.packageName}.fileprovider"
        val uri = FileProvider.getUriForFile(activity, authority, tempFile)

        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
            .putExtra(MediaStore.EXTRA_OUTPUT, uri)

        activity.launchForResult(intent) { resultCode, _ ->
            if (resultCode == Activity.RESULT_OK && tempFile.exists()) {
                onComplete(tempFile.absolutePath)
            } else {
                onComplete(null)
            }
        }
    }

    actual fun pickFromAlbum(maxCount: Int, onComplete: (List<String>) -> Unit) {
        val activity = currentActivity() as? KuiklyActivity ?: run {
            onComplete(emptyList()); return
        }
        val intent = Intent(Intent.ACTION_PICK)
            .setType("image/*")
            .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, maxCount > 1)

        activity.launchForResult(intent) { code, data ->
            if (code != Activity.RESULT_OK || data == null) {
                onComplete(emptyList()); return@launchForResult
            }
            val paths = mutableListOf<String>()
            data.clipData?.let { clip ->
                for (i in 0 until clip.itemCount.coerceAtMost(maxCount)) {
                    paths += copyUriToCache(clip.getItemAt(i).uri)
                }
            } ?: data.data?.let { paths += copyUriToCache(it) }
            onComplete(paths)
        }
    }

    actual fun hasPermission(): Boolean {
        return ContextCompat.checkSelfPermission(
            appContext, Manifest.permission.CAMERA
        ) == PackageManager.PERMISSION_GRANTED
    }

    actual companion object {
        actual const val MODULE_NAME = "Camera"
    }
}
*/

// ═══════════════════════════════════════════════════════════════════
// 步骤 ③ iosMain:iOS 实现(伪代码)
// 文件位置:shared/src/iosMain/kotlin/com/example/kuikly/modules/CameraModule.kt
// ═══════════════════════════════════════════════════════════════════
/*
package com.example.kuikly.modules

import platform.UIKit.*
import platform.AVFoundation.*
import platform.Foundation.*

actual class CameraModule actual constructor() : Module() {

    actual fun takePhoto(onComplete: (String?) -> Unit) {
        if (!hasPermission()) {
            requestCameraPermission { granted ->
                if (granted) takePhoto(onComplete) else onComplete(null)
            }
            return
        }
        val picker = UIImagePickerController().apply {
            sourceType = UIImagePickerControllerSourceTypeCamera
            delegate = createDelegate(onComplete)
        }
        currentVC()?.presentViewController(picker, animated = true, completion = null)
    }

    actual fun pickFromAlbum(maxCount: Int, onComplete: (List<String>) -> Unit) {
        // 用 PHPickerViewController(iOS 14+)实现多选相册
    }

    actual fun hasPermission(): Boolean {
        val status = AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo)
        return status == AVAuthorizationStatusAuthorized
    }

    actual companion object {
        actual const val MODULE_NAME = "Camera"
    }
}
*/

// ═══════════════════════════════════════════════════════════════════
// 步骤 ④ 注册 + 业务调用(commonMain 业务侧)
// ═══════════════════════════════════════════════════════════════════
/*
// (a) Application 启动时注册(Android)
class KuiklyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        ModuleRegistry.register(CameraModule.MODULE_NAME) { CameraModule() }
    }
}

// (b) 业务 Pager 调用
@Page("PublishPage")
internal class PublishPage : Pager() {
    private var photoPath by observable<String?>(null)

    override fun body(): ViewBuilder {
        val ctx = this
        return {
            View {
                attr {
                    height(200f); allCenter()
                    backgroundColor(Color(0xFFE0E0E0L))
                    borderRadius(12f); margin(16f)
                }
                event {
                    click {
                        acquireModule<CameraModule>(CameraModule.MODULE_NAME)
                            .takePhoto { path -> ctx.photoPath = path }
                    }
                }
                if (ctx.photoPath == null) {
                    Text { attr { text("📷 点击拍照"); fontSize(16f) } }
                } else {
                    Image {
                        attr { src(ctx.photoPath!!); flex(1f); resizeCover() }
                    }
                }
            }
        }
    }
}
*/
kotlin
/**
 * 第 8 章配套代码 · 平台通信综合 demo
 *
 * 文件位置:shared/src/commonMain/kotlin/com/example/kuikly/pages/PlatformBridgeDemo.kt
 *
 * 演示 5 个内置 Module 的常用调用方式:
 *   - ToastModule           弹 toast
 *   - StorageModule         本地 KV 存储
 *   - HttpModule            网络请求
 *   - PageRouterModule      页面跳转
 *   - NotificationModule    跨 Pager 广播
 */

package com.example.kuikly.pages

import com.tencent.kuikly.core.annotations.Page
import com.tencent.kuikly.core.base.Color
import com.tencent.kuikly.core.base.ViewBuilder
import com.tencent.kuikly.core.module.HttpModule
import com.tencent.kuikly.core.module.NotificationModule
import com.tencent.kuikly.core.module.PageRouterModule
import com.tencent.kuikly.core.module.SharedPreferencesModule
import com.tencent.kuikly.core.module.ToastModule
import com.tencent.kuikly.core.nvi.serialization.json.JSONObject
import com.tencent.kuikly.core.pager.Pager
import com.tencent.kuikly.core.reactive.handler.observable
import com.tencent.kuikly.core.views.Text
import com.tencent.kuikly.core.views.View

@Page("PlatformBridgeDemo")
internal class PlatformBridgeDemo : Pager() {

    private var fetchResult by observable("(点击下方按钮发起请求)")
    private var savedToken by observable("")
    private var loading by observable(false)

    // ─── 缓存常用 Module 引用 ─────
    private val toast by lazy { acquireModule<ToastModule>(ToastModule.MODULE_NAME) }
    private val sp by lazy { acquireModule<SharedPreferencesModule>(SharedPreferencesModule.MODULE_NAME) }
    private val http by lazy { acquireModule<HttpModule>(HttpModule.MODULE_NAME) }
    private val router by lazy { acquireModule<PageRouterModule>(PageRouterModule.MODULE_NAME) }
    private val notification by lazy { acquireModule<NotificationModule>(NotificationModule.MODULE_NAME) }

    // ─── 用 NotificationModule 监听跨页面广播 ─────
    override fun created() {
        super.created()

        savedToken = sp.getItem("auth_token") ?: ""

        notification.addNotificationListener("UserLoggedIn") { params ->
            val token = params?.optString("token") ?: ""
            savedToken = token
            sp.setItem("auth_token", token)
            toast.showToast("已收到登录广播:$token")
        }
    }

    override fun pageWillDestroy() {
        super.pageWillDestroy()
        // ⚠️ 必须解绑,避免内存泄漏
        notification.removeNotificationListener("UserLoggedIn")
    }

    override fun body(): ViewBuilder {
        val ctx = this
        return {
            attr {
                backgroundColor(Color(0xFFF5F5F7L))
                paddingTop(40f)
                paddingHorizontal(16f)
            }

            sectionTitle("Platform Bridge 综合演示")

            // ─── 1. ToastModule ─────────────────
            actionButton(
                title = "1. 弹个 Toast (ToastModule)",
                color = 0xFF1976D2L,
                onClick = { ctx.toast.showToast("Hello from Kuikly! 🎉") },
            ).invoke(this)

            // ─── 2. StorageModule ───────────────
            actionButton(
                title = "2. 保存 token (StorageModule)",
                color = 0xFF388E3CL,
                onClick = {
                    val newToken = "tk_${kotlin.random.Random.nextInt(1000, 9999)}"
                    ctx.sp.setItem("auth_token", newToken)
                    ctx.savedToken = newToken
                    ctx.toast.showToast("已保存:$newToken")
                },
            ).invoke(this)

            Text {
                attr {
                    text("当前 token:${ctx.savedToken.ifEmpty { "(无)" }}")
                    fontSize(12f); color(Color(0xFF9E9E9EL))
                    paddingHorizontal(8f); marginBottom(12f)
                }
            }

            // ─── 3. HttpModule ──────────────────
            actionButton(
                title = if (ctx.loading) "3. 请求中…" else "3. 拉一次接口 (HttpModule)",
                color = 0xFFFF5722L,
                disabled = ctx.loading,
                onClick = { ctx.fetchUser() },
            ).invoke(this)

            View {
                attr {
                    backgroundColor(Color.WHITE)
                    padding(12f)
                    borderRadius(8f)
                    marginBottom(12f)
                }
                Text {
                    attr {
                        text(ctx.fetchResult)
                        fontSize(12f); color(Color(0xFF424242L))
                        lineHeight(18f)
                    }
                }
            }

            // ─── 4. PageRouterModule ────────────
            actionButton(
                title = "4. 跳到 DetailPage (RouterModule)",
                color = 0xFF7F52FFL,
                onClick = {
                    ctx.router.openPage(
                        "DetailPage",
                        JSONObject().apply { put("itemId", "abc-${kotlin.random.Random.nextInt(100)}") }
                    )
                },
            ).invoke(this)

            // ─── 5. NotificationModule ──────────
            actionButton(
                title = "5. 广播 UserLoggedIn 事件",
                color = 0xFFE91E63L,
                onClick = {
                    ctx.notification.postNotification(
                        "UserLoggedIn",
                        JSONObject().apply {
                            put("token", "broadcast_${kotlin.random.Random.nextInt(1000)}")
                        }
                    )
                },
            ).invoke(this)
        }
    }

    // ─── 抽出来的 ViewBuilder ────────────
    private fun sectionTitle(title: String): ViewBuilder = {
        Text {
            attr {
                text(title)
                fontSize(20f)
                fontWeightBold()
                color(Color.BLACK)
                marginBottom(20f)
            }
        }
    }

    private fun actionButton(
        title: String,
        color: Long,
        disabled: Boolean = false,
        onClick: () -> Unit,
    ): ViewBuilder = {
        View {
            attr {
                height(48f)
                allCenter()
                backgroundColor(Color(color))
                borderRadius(8f)
                marginBottom(8f)
                opacity(if (disabled) 0.5f else 1f)
            }
            event { click { if (!disabled) onClick() } }

            Text {
                attr {
                    text(title)
                    color(Color.WHITE)
                    fontSize(14f)
                    fontWeightBold()
                }
            }
        }
    }

    private fun fetchUser() {
        loading = true
        fetchResult = "请求中…"
        http.request(
            url = "https://jsonplaceholder.typicode.com/users/1",
            method = "GET",
            params = JSONObject(),
            headers = JSONObject(),
        ) { result ->
            loading = false
            if (result == null) {
                fetchResult = "❌ 请求失败"
                return@request
            }
            val name = result.optString("name", "Unknown")
            val email = result.optString("email", "")
            fetchResult = "✅ 收到响应:\n姓名: $name\n邮箱: $email"
        }
    }
}

CameraModule.kt ↗ · PlatformBridgeDemo.kt ↗