主题
第 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,正确做法:
- 简单 API:用 expect/actual
- 复杂能力:定义 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 步法:
- commonMain 定义
expect class CameraModule : Module { fun takePhoto(cb) } - androidMain 写 actual 实现:调
MediaStore.ACTION_IMAGE_CAPTURE - iosMain 写 actual 实现:用
UIImagePickerController - Application 启动时
ModuleRegistry.register("Camera") { CameraModule() } - 业务侧
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"
}
}
}