Contents
- Core Guidelines
- Workflow: Creating Custom Module (Kuikly Side)
- Workflow: Implementing Native Side Module
Core Guidelines
- Module is the unified interface for platform APIs: Kuikly itself does not have platform-related capabilities; through the Module mechanism, Native APIs are exposed for the Kuikly side to call.
- A custom Module requires implementation on both sides: The Kuikly side defines the API interface, and the Native side (Android/iOS/HarmonyOS/H5/Mini Program) implements the specific logic, associated through
moduleNameassociation. - Communication methods are divided into synchronous and asynchronous: Synchronous calls (
syncToNativeMethod) execute on the Kuikly thread, asynchronous calls (asyncToNativeMethod) execute on the main thread. Synchronous calls should avoid time-consuming operations. - The Module name must be consistent across all platforms: On the Kuikly side,
moduleName()the return value must exactly match the name registered on the Native side.
Workflow: Creating Module (Kuikly Side)
Use this workflow to create a custom Module on the Kuikly (Kotlin) side.
Steps:
- Create a new Module class that inherits
Module, and implement themoduleName()method. - Write business API methods, and communicate with Native through
toNative/syncToNativeMethod/asyncToNativeMethod. - Register the Module in the Pager's
createExternalModules(). - Obtain and use it in business code through
acquireModule.
Step 1: Define the Module class
import com.tencent.kuikly.core.module.Module
import com.tencent.kuikly.core.module.CallbackFn
import com.tencent.kuikly.core.nvi.serialization.json.JSONObject
class MyLogModule : Module() {
override fun moduleName(): String = "KRMyLogModule"
companion object {
const val MODULE_NAME = "KRMyLogModule"
}
}
Step 2: Write API methods
Module provides the following communication methods:
| Method | Call Type | Native Execution Thread | Parameter Type | Callback Type |
|---|---|---|---|---|
syncToNativeMethod(methodName, JSONObject?, CallbackFn?) |
Synchronous | Kuikly thread | JSONObject (serialized to JSON string) | CallbackFn(JSONObject?) |
syncToNativeMethod(methodName, Array<Any>, AnyCallbackFn?) |
Synchronous | Kuikly thread | Primitive type array (String/Int/Float/ByteArray) | AnyCallbackFn(Any?) |
asyncToNativeMethod(methodName, JSONObject?, CallbackFn?) |
Asynchronous | Main thread | JSONObject (serialized to JSON string) | CallbackFn(JSONObject?) |
asyncToNativeMethod(methodName, Array<Any>, AnyCallbackFn?) |
Asynchronous | Main thread | Primitive type array (String/Int/Float/ByteArray) | AnyCallbackFn(Any?) |
toNative(keepCallbackAlive, methodName, param, callback, syncCall) |
Generic low-level method | Depends on syncCall | Any? | CallbackFn(JSONObject?) |
Asynchronous call (no return value):
fun log(content: String) {
asyncToNativeMethod(
"log",
JSONObject().apply { put("content", content) },
null
)
}
Asynchronous call (with callback):
fun logWithCallback(content: String, callbackFn: CallbackFn) {
asyncToNativeMethod(
"logWithCallback",
JSONObject().apply { put("content", content) },
callbackFn
)
}
Synchronous call (with return value):
fun syncLog(content: String): String {
return syncToNativeMethod(
"syncLog",
JSONObject().apply { put("content", content) },
null
)
}
Binary data transfer (using the Array parameter):
fun uploadData(bytes: ByteArray): Any? {
return syncToNativeMethod(
"uploadData",
arrayOf<Any>(bytes),
null
)
}
Resident callback (keepCallbackAlive = true):
fun registerListener(callbackFn: CallbackFn): CallbackRef {
val result = toNative(
true, // keepCallbackAlive: callback 不会被自动销毁
"registerListener",
null,
callbackFn,
false
)
return result.callbackRef!!
}
// 手动移除回调
fun unregisterListener(callbackRef: CallbackRef) {
removeCallback(callbackRef)
}
Step 3: Register the Module
A custom Module needs to be registered in the Pager or ComposeContainer's createExternalModules() , and the registration method is the same for both DSLs.
Kuikly DSL registration:
import com.tencent.kuikly.core.annotations.Page
import com.tencent.kuikly.core.module.Module
@Page("MyPage")
internal class MyPage : Pager() {
override fun createExternalModules(): Map<String, Module>? {
return mapOf(
MyLogModule.MODULE_NAME to MyLogModule()
)
}
// 如果有基类 BasePager 已注册了其他 Module,需要合并:
// override fun createExternalModules(): Map<String, Module>? {
// val modules = (super.createExternalModules() as? HashMap) ?: hashMapOf()
// modules[MyLogModule.MODULE_NAME] = MyLogModule()
// return modules
// }
override fun body(): ViewBuilder { ... }
}
Compose DSL registration:
import com.tencent.kuikly.compose.ComposeContainer
import com.tencent.kuikly.compose.setContent
import com.tencent.kuikly.core.annotations.Page
import com.tencent.kuikly.core.module.Module
@Page("MyComposePage")
class MyComposePage : ComposeContainer() {
// 注册方式与 Kuikly DSL 完全一致
override fun createExternalModules(): Map<String, Module>? {
return mapOf(
MyLogModule.MODULE_NAME to MyLogModule()
)
}
override fun willInit() {
super.willInit()
setContent {
MyScreen()
}
}
}
Step 4: Use the Module
Usage in Kuikly DSL:
override fun created() {
super.created()
val myLogModule = acquireModule<MyLogModule>(MyLogModule.MODULE_NAME)
// 异步调用
myLogModule.log("test log")
// 异步调用带回调
myLogModule.logWithCallback("log with callback") { data ->
val result = data // Native 侧返回的 JSONObject
}
// 同步调用
val result = myLogModule.syncLog("sync log")
}
Usage in Compose DSL:
import androidx.compose.runtime.Composable
import com.tencent.kuikly.compose.ui.platform.LocalActivity
import com.tencent.kuikly.core.pager.Pager
@Composable
fun MyScreen() {
val pager = LocalActivity.current.getPager() as Pager
val myLogModule = pager.acquireModule<MyLogModule>(MyLogModule.MODULE_NAME)
// 异步调用
myLogModule.log("test log")
// 异步调用带回调
myLogModule.logWithCallback("log with callback") { data ->
val result = data // Native 侧返回的 JSONObject
}
// 同步调用
val result = myLogModule.syncLog("sync log")
}
Workflow: Implementing Native Side Module
Use this workflow to implement the Native side of a custom Module.
Prerequisites: You only need to implement the Native-side Module on platforms that the business needs to support; there is no need to implement all platforms.
Steps:
- Determine the target platforms that need to be supported (Android/iOS/HarmonyOS/H5/Mini Program).
- Create the corresponding Module class in the host project of the target platform.
- Implement the
callmethod, and dispatch processing according tomethod. - Register the Module with the Kuikly framework.
💡 HarmonyOS platform note: HarmonyOS provides two implementation approaches, ArkTS and C; in general, you only need to choose the ArkTS implementation.
For platform-specific implementations, see MODULE_IMPLEMENT.md
Quick reference: Module base classes and registration methods for each platform
| Platform | Base class | Registration method | Notes |
|---|---|---|---|
| Android | KuiklyRenderBaseModule() |
registerExternalModule in moduleExport(name) { Module() } |
Override call(method, params: Any?, callback) or call(method, params: String?, callback) |
| iOS | KRBaseModule |
The class name must match the moduleName on the Kuikly side (dynamically created at runtime) | The method name must match the methodName on the Kuikly side, and the parameter is fixed as NSDictionary, Swift requires the @objc annotation |
| HarmonyOS (ArkTS) | KuiklyRenderBaseModule |
getCustomRenderModuleCreatorRegisterMap register in |
Must implement syncMode() to specify synchronous/asynchronous mode |
| HarmonyOS (C) | KRRenderModuleRegisterV2 |
InitKuikly register in |
The return value KRAnyData is released by the framework |
| H5 | KuiklyRenderBaseModule() |
registerExternalModule in moduleExport(name) { Module() } |
Same as Android |
| Mini Program | KuiklyRenderBaseModule() |
registerExternalModule in moduleExport(name) { Module() } |
Can access the NativeApi.plat object via wx object |
Data types supported on the Native side
| Platform | Supported data types |
|---|---|
| Android | String Int Long Float Double Boolean ByteArray Map List JSONObject |
| iOS | NSString NSNumber BOOL NSData NSDictionary NSArray |
| HarmonyOS | String Int Long Float Double Bool ByteArray Array Map/Record |
| H5/Mini Program | String Int Long Float Double Boolean Array Map List JSONObject JSONArray |
Data serialization rules
| Category | Serialization method | Involved types |
|---|---|---|
| Basic types | Direct pass-through | String Int Float Double Boolean NSNumber |
| Binary data | Direct pass-through | ByteArray NSData ArrayBuffer |
| JSON data | JSON string | JSONObject JSONArray |
| Collection types | JSON string | Map/Record List NSDictionary NSArray Array |
| Special rules | Direct pass-through | When Array contains ByteArray/NSData when |
⚠️
syncToNativeMethodandasyncToNativeMethodWhen passing a JSONObject parameter, it will be serialized into a JSON string, and ByteArray binary data is not supported. To transmit binary, use theArray<Any>parameter overload method.
Common pitfalls and correct practices
| ❌ Incorrect practice | ✅ Correct practice |
|---|---|
| The Module name on the Kuikly side is inconsistent with the Native side | Ensure that the moduleName() return value exactly matches the Native registration name |
Get the Module before created Get Module before |
Get the Module at or after created() (Kuikly DSL); get it after willInit in setContent (Compose DSL) |
| Performing time-consuming operations in synchronous calls | Synchronous calls execute on the Kuikly thread; avoid time-consuming operations blocking rendering |
Forgetting to register the custom Module in createExternalModules Register custom Module in |
The custom Module must be registered in the Pager's createExternalModules() register in |
| Using a JSONObject parameter to transmit binary data | Use the Array<Any> of the syncToNativeMethod/asyncToNativeMethod parameter to transmit ByteArray |
| iOS-side Module class name does not match Kuikly side | iOS dynamically creates Module by class name, and the class name must match moduleName() be consistent |
Implemented in Swift on iOS side but missing @objc annotation |
A Module implemented in Swift requires @objc or @objcMembers modifier |
| Resident callback forgotten to be manually removed | keepCallbackAlive=true The callback of needs to be called before the page is destroyed removeCallback |
Call directly in Compose DSL acquireModule |
In @Composable in the function, obtain via LocalActivity.current.getPager().acquireModule get |
Forgot to import in Compose DSL LocalActivity |
requires import com.tencent.kuikly.compose.ui.platform.LocalActivity |