鸿蒙工程接入

大约 13 分钟

鸿蒙工程接入

注意

在此之前请确保已经完成KMP侧 Kuikly的接入,如还未完成,请移步Kuikly KMP侧接入

完成Kuikly KMP侧的配置后, 我们还需要将Kuikly渲染器和适配器接入到宿主平台中,此文档适用于您想把Kuikly渲染器接入到您现有的鸿蒙工程中。下面我们来看下,如何在现有鸿蒙工程中接入Kuikl渲染器。

我们用鸿蒙开发IDE DevEco Studio新建一个名为KuiklyTest的新工程并假设这个工程是你现有的鸿蒙工程:

添加Kuikly渲染器依赖

编辑entry模块的oh-package.json5,添加 Kuikly 相关 dependencies 依赖项:

// entry/oh-package.json5
{
  ...
  "dependencies": {
    ...
    "@kuikly-open/render": 'KUIKLY_RENDER_VERSION'
  }
}

提示

点击右上角【Sync Now】(或者在entry目录下命令行执行ohpm install)。

创建鸿蒙运行时初始化接口

Kuikly 鸿蒙端渲染是基于ArkUI C-API 实现,在业务接入时,需要通过 NAPI ,将运行时初始化接口暴露到业务ArkTS层。

添加C++(NAPI)支持

在鸿蒙工程的 entry 入口模块添加C++(NAPI)支持(以前加过的跳过)。右键点击 entry 目录,在弹出的菜单中做如下选择:

确认创建后,会在 entry 目录新增 C++代码目录,如下图:

添加 NAPI 初始化入口函数

在上述C++目录下的napi_init.cpp文件,添加InitKuikly初始化入口,并暴露给ArkTS。具体实现代码,请参考源码工程 core-render-ohos/entry 模块的napi_init.cpp类。

// entry/src/main/cpp/napi_init.cpp
#include "napi/native_api.h"

static napi_value InitKuikly(napi_env env, napi_callback_info info) {
    //  添加业务代码初始化逻辑。具体见后续步骤说明
    return nullptr;
}

EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
    napi_property_descriptor desc[] = {
        // 导出 initKuikly,使其可以被ArkTS层访问和调用
        {"initKuikly", nullptr, InitKuikly, nullptr, nullptr, nullptr, napi_default, nullptr},
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END

static napi_module demoModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "entry",
    .nm_priv = ((void*)0),
    .reserved = { 0 },
};

extern "C" __attribute__((constructor)) void RegisterEntryModule(void)
{
    napi_module_register(&demoModule);
}

在C++目录下的index.d.ts文件,对initKuikly进行接口声明

// entry/src/main/cpp/types/libentry/index.d.ts
export const initKuikly: () => number;

关联NativeManager

entry/src/main/ets下创建kuikly目录。
kuikly目录下,创建MyNativeManager.ets类,实现KuiklyNativeManager类的loadNative接口,将Kuikly运行时初始化入口与框架KuiklyNativeManager关联。
请参考源码工程 core-render-ohos/entry 模块的MyNativeManager.ets类。

// entry/src/main/ets/kuikly/MyNativeManager.ets
import { KuiklyNativeManager } from '@kuikly-open/render';
import Napi from 'libentry.so';

class MyNativeManager extends KuiklyNativeManager {
  protected loadNative(): number {
    // 调用Napi接口,初始化 Kuikly Native
    return Napi.initKuikly();
  }
}

// 导出一个全局的 KuiklyNativeManager 实例给 Kuikly 页面共用
const globalNativeManager = new MyNativeManager();

export default globalNativeManager;

实现Kuikly承载容器

创建委托类

kuikly目录下,创建IKuiklyViewDelegate委托者实现类KuiklyViewDelegate.ets,用于向框架注册自定义View和Module、框架感知页面生命周期等。
请参考源码工程 core-render-ohos/entry 模块的KuiklyViewDelegate.ets类,注入自定义 View 和 Module。

// entry/src/main/ets/kuikly/KuiklyViewDelegate.ets
import { IKuiklyViewDelegate, KRRenderModuleExportCreator, KRRenderViewExportCreator } from '@kuikly-open/render';

export class KuiklyViewDelegate extends IKuiklyViewDelegate {
  getCustomRenderViewCreatorRegisterMap(): Map<string, KRRenderViewExportCreator> {
    const map: Map<string, KRRenderViewExportCreator> = new Map();
    return map;
  }

  getCustomRenderModuleCreatorRegisterMap(): Map<string, KRRenderModuleExportCreator> {
    const map: Map<string, KRRenderModuleExportCreator> = new Map();
    return map;
  }
}

委托类说明

可以重写相关方法,实现自定义、扩展、配置 Kuikly 等功能。

export abstract class IKuiklyViewDelegate extends KRNativeRenderController {
  /**
   * 获取自定义扩展渲染视图创建注册Map
   */
  abstract getCustomRenderViewCreatorRegisterMap(): Map<string, KRRenderViewExportCreator>;

  /**
   * 获取自定义扩展渲染视图创建注册Map。
   * 通过这个方式注册的creator,创建的自定义view将不会有影子节点处理基础事件,需要用户在arkts侧响应所有属性的设置。
   * 当这个方式的好处是,由于不存在影子节点,其view曾经和DSL中定义的是保持一致的。
   * 建议仅在有强一致层级需求的时候才采用。
   */
  getCustomRenderViewCreatorRegisterMapV2(): Map<string, KRRenderViewExportCreator>{
    // by default
    return new Map();
  }
  
  /**
   * 获取自定义扩展module创建注册Map
   */
  abstract getCustomRenderModuleCreatorRegisterMap(): Map<string, KRRenderModuleExportCreator>;

  /**
   * 获取自定义[KuiklyRenderView]生命周期回调
   */
  getKuiklyRenderViewLifecycleCallback(): IKuiklyRenderViewLifecycleCallback | null {
    return null
  }

  /**
   * Kuikly框架设置性能监控选项,默认只开启动监控
   * @return Array<KRMonitorType>: 需要设置的性能监控选项列表(目前仅支持启动监控)
   */
  performanceMonitorTypes(): Array<KRMonitorType> {
    return [KRMonitorType.LAUNCH];
  }

  /**
   * 回调启动数据
   */
  onGetLaunchData(data: Record<string, number>): void {
  }

  /**
   * 回调性能数据
   */
  onGetPerformanceData(data: Record<string, number>): void {
  }

  /**
   * 字体缩放是否跟随系统
   * @return boolean true:跟随系统(默认) false:不跟随系统(缩放比例为1)
   */
  fontSizeScaleFollowSystem(): boolean {
    return true
  }

}

实现Kuikly承载容器

在page页面容器中加入Kuikly组件(以 pages/Index 为例,也可以是新建的page),触发Kuikly页面加载。

Kuikly组件参数说明

  • pageName: 页面名称,对应@Page注解中定义的名称
  • pageData: 页面数据,传递给Kuikly页面的参数
  • delegate: 委托者实现,用于注册自定义View和Module
  • initialSize: 初始尺寸设置,用于指定Kuikly容器的初始宽高(可选参数)
    • 格式:{ width: number, height: number }
    • 用途:初始化时传入正确的容器尺寸,可以提前跨端页面的创建(传入错误值会导致重复排版和布局跳变)
  • onControllerReadyCallback: 控制器就绪回调
  • nativeManager: 原生管理器实例


请参考源码工程 core-render-ohos/entry 模块的Index.ets类。

// entry/src/main/ets/kuikly/pages/Index.ets
import { KRRecord, KRNativeRenderController, Kuikly } from '@kuikly-open/render';
import globalNativeManager from '../kuikly/MyNativeManager';
import { KuiklyViewDelegate } from '../kuikly/KuiklyViewDelegate';
import router from '@ohos.router';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { ContextCodeHandler } from '../kuikly/ContextCodeHandler';

@Entry
@Component
struct Index {
  private kuiklyViewDelegate = new KuiklyViewDelegate();
  private kuiklyController : KRNativeRenderController | null = null

  private pageName: string | null = null;
  private pageData?: KRRecord;
  private contextCode: string = '';
  private contextCodeHandler: ContextCodeHandler = new ContextCodeHandler();
  private useDefaultBackPress = true
  @State showKuikly: boolean = false;

  onBackPress(): boolean | void {
    if(this.useDefaultBackPress){
      return
    }

    if(this.kuiklyViewDelegate){
      this.kuiklyController?.sendBackPressEvent()
      return true
    }
  }

  aboutToAppear(): void {
    const params = router.getParams() as Record<string, Object>;
    this.pageName = params?.pageName as string;
    this.pageData = (params?.pageData as KRRecord | null) ?? {}
    if (this.contextCodeHandler.isNeedGetContextCode(params)) {
      this.contextCodeHandler.handleGetContextCode(getContext(), params, (contextCode) => {
        this.contextCode = contextCode;
        this.showKuikly = true;
      }, (stack) => {
        this.showExceptionDialog(stack);
      })
    } else {
      this.showKuikly = true;
    }
  }

  build() {
    Stack() {
      if (this.showKuikly) {
        Kuikly({
          pageName: this.pageName ?? 'router',
          pageData: this.pageData ?? {},
          delegate: this.kuiklyViewDelegate,
          contextCode: this.contextCode,
          executeMode: this.contextCodeHandler.getExecuteMode(this.contextCode),
          // 可选:设置Kuikly容器的初始尺寸
          // initialSize: { width: this.calculateWidth(), height: this.calculateHeight() },
          onControllerReadyCallback: (controller) => {
            this.kuiklyController = controller
            controller.registerExceptionCallback((executeMode, stack) => {
              this.showExceptionDialog(stack);
              const stackInfo:KRRecord = JSON.parse(stack)
              stackInfo['stack'].toString().split('\n').forEach((it)=>{
                hilog.error(0x0000, 'demo', '%{public}s', it);
              })
            });
          },
          nativeManager: globalNativeManager,
        })
      }
    }.expandSafeArea([SafeAreaType.KEYBOARD])
    // .backgroundColor(Color.Green)
  }

  private showExceptionDialog(stack: string) {
    // 对话框显示异常堆栈
  }

  onPageShow(): void {
    const res = getContext(this).resourceDir
    this.kuiklyViewDelegate.pageDidAppear()
  }

  onPageHide(): void {
    this.kuiklyViewDelegate.pageDidDisappear()
  }
}
// entry/src/main/ets/kuikly/pages/ContextCodeHandler.ets
import { KRRenderExecuteModeBase, KRRenderNativeMode } from '@kuikly-open/render';

export class ContextCodeHandler {
  isNeedGetContextCode(params: Record<string, Object>) {
    return false
  }

  handleGetContextCode(context: Context, params: Record<string, Object>, callback: (contextCodeParam: string) => void,
  exceptionCallback: (stack: string) => void) {

  }

  getExecuteMode(contextCode: string): KRRenderExecuteModeBase {
    return KRRenderNativeMode.Native
  }
}

实现适配器(必须实现部分)

Kuikly框架为了灵活和可拓展性,不会内置实现异常处理,日志实现等功能,而是通过适配器的设计模式,将具体实现委托给宿主App实现。

Kuikly为鸿蒙端宿主工程提供了以下适配器, 需宿主平台按需实现

  1. 日志适配器: 用于给Kuikly框架和Kuikly业务实现日志打印。推荐宿主侧实现
  2. 页面路由适配器: 用于实现跳转到Kuikly容器。宿主侧必须实现
  3. PAG加载适配器: 用于给Kuikly提供PAG加载的能力。宿主按需实现(使用PAG组件时必须实现,可参考AppKRPAGAdapter.etsopen in new window

日志适配器示例

请参考源码工程 core-render-ohos/entry 模块的AppKRLogAdapter.ets类。

// entry/src/main/ets/kuikly/adapters/AppKRLogAdapter.ets
import { IKRLogAdapter } from '@kuikly-open/render';
import { hilog } from '@kit.PerformanceAnalysisKit';

export class AppKRLogAdapter implements IKRLogAdapter {

  i(tag: string, msg: string): void {
    hilog.info(0x30, tag, '%{public}s', msg)
  }

  d(tag: string, msg: string): void {
    hilog.debug(0x30, tag, '%{public}s', msg)
  }

  e(tag: string, msg: string): void {
    hilog.error(0x30, tag, '%{public}s', msg)
  }

}

路由适配器示例

请参考源码工程 core-render-ohos/entry 模块的AppKRRouterAdapter.ets类。

// entry/src/main/ets/kuikly/adapters/AppKRRouterAdapter.ets
import { KRRecord } from '@kuikly-open/render';
import { IKRRouterAdapter } from '@kuikly-open/render';
import router from '@ohos.router';
import { common } from '@kit.AbilityKit';

export class AppKRRouterAdapter implements IKRRouterAdapter {

  openPage(context: common.UIAbilityContext, pageName: string, pageData: KRRecord): void {
    router.pushUrl({
      url: 'pages/Index',
      params: {
         pageName,
         pageData
      }
    })
  }

  closePage(context: common.UIAbilityContext): void {
    router.back()
  }
}

初始化适配器

在 UIAbility 的 onWindowStageCreate 时机初始化 Kuikly(多ability场景可以把初始化时机提前到AbilityStage,避免相互覆盖):
请参考源码工程 core-render-ohos/entry 模块的EntryAbility.ets类。

// entry/src/main/ets/entryability/EntryAbility.ets
import { KuiklyRenderAdapterManager } from '@kuikly-open/render';

export default class EntryAbility extends UIAbility {
  ...
  onWindowStageCreate(windowStage: window.WindowStage): void {
    // Main window is created, set main page for this ability
    // 日志适配器
    KuiklyRenderAdapterManager.krLogAdapter = new AppKRLogAdapter();
    // 路由适配器
    KuiklyRenderAdapterManager.krRouterAdapter = new AppKRRouterAdapter();
    ...
  }
}

链接Kuikly业务代码

Kuikly业务代码,在鸿蒙平台上会被编译成 so 产物,下面以本地so文件方式为例介绍链接Kuikly业务代码的流程。 我们先前在KuiklyKMP跨端工程接入中已经新建了Kuikly业务工程,然后我们将这个业务工程的业务代码编译成的.so链接到我们的现有鸿蒙工程。

生成 so 产物和头文件

鸿蒙Kuikly业务代码编译生成 so 产物,详细步骤参考鸿蒙平台开发方式
KMP侧接入工程中,编译跨端工程的shared模块,命令行执行 ./gradlew -c settings.ohos.gradle.kts :shared:linkOhosArm64 编译鸿蒙so产物。

拷贝Kuikly业务代码产物

将业务代码生成的动态链接库文件libshared.so和头文件libshared_api.h拷贝到C++模块中:

修改CMakeList

修改C++目录下 CMakeLists.txt,导入业务产物和Kuikly SDK的动态链接库:

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})

# Kuikly SDK
add_library(kuikly_render ALIAS render::kuikly)
# 业务产物
add_library(kuikly_shared SHARED IMPORTED)
set_target_properties(kuikly_shared
    PROPERTIES
    IMPORTED_LOCATION ${NATIVERENDER_ROOT_PATH}/../../../libs/${OHOS_ARCH}/libshared.so)
# 追加「kuikly_shared」和「kuikly_render」到入口模块target_link_libraries
target_link_libraries(entry PUBLIC libace_napi.z.so kuikly_shared kuikly_render)

实现 NAPI 初始化入口函数InitKuikly

在前述章节添加 NAPI 初始化入口函数步骤,我们创建了InitKuikly初始化入口函数,我们在这个步骤实现这个函数即可。

// entry/src/main/cpp/napi_init.cpp
#include "libshared_api.h"
#include "napi/native_api.h"

static napi_value InitKuikly(napi_env env, napi_callback_info info) {
  // symbols入口名和kuikly工程的配置有关,具体查看产物的头文件
  auto api = libshared_symbols();
  int handler = api->kotlin.root.initKuikly();
  napi_value result;
  napi_create_int32(env, handler, &result);
  return result;
}
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
  napi_property_descriptor desc[] = {
    {"initKuikly", nullptr, InitKuikly, nullptr, nullptr, nullptr, napi_default, nullptr},
  };
  napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
  return exports;
}
EXTERN_C_END

拷贝资源文件

如果有资源文件,需要把assets目录下的资源文件拷贝到entry/src/main/resources/resfile,例如:

shared/src/commonMain/assets/common/* -> entry/src/main/resources/resfile/common/*

编写TestPage验证

完成上述步骤后, 我们便完成了Kuikly的接入。下面我们在KMP侧接入工程中的shared模块下新建页面名为test的TestPage进行测试。

@Page("test")
class TestPage : Pager(){
    override fun body(): ViewBuilder {
        return {
            attr {
                allCenter()
            }

            Text {
                attr {
                    fontSize(18f)
                    text("Hello Kuikly")
                    color(Color.GREEN)
                }
            }
        }
    }
}

test替换router作为pageName传入page/Index中, 指定跳转到我们刚新建的TestPage页面

// entry/src/main/ets/kuikly/pages/Index.ets
...
      Kuikly({
        pagerName: this.pageName ?? 'test',
        ...
      })
...

参考生成 so 产物和头文件拷贝Kuikly业务代码产物,在KMP工程重新生成 so 产物和头文件,更新到鸿蒙工程,编译鸿蒙应用。 当手机出现以下界面时, 说明已经成功接入Kuikly

实现适配器(按需实现部分)

图片加载适配器示例

该适配器用于给Kuikly的Image组件实现自定义图片加载能力,非必须实现, 业务可根据实际使用需求来决定是否实现。

接口定义于 Kuikly.h:

/**
 * @brief 业务图片加载完成后,用于回调给kuikly的函数指针
 * @param context 上下文
 * @param src image组件设置的src属性
 * @param image_descriptor 解码好的图片
 * @param new_src 新的src地址,比如从原src映射到一个新的src路径
 * @discuss 当image_descriptor非空时,kuikly优先用image_descriptor,其次再使用new_src
 */
typedef void (*KRSetImageCallback)(const void* context,
                                   const char *src,
                                   ArkUI_DrawableDescriptor *image_descriptor,
                                   const char *new_src);
/**
 * @brief 自定义image adapter
 * @param context 上下文
 * @param src image组件设置的src属性
 * @param callback 自定义加载图片完成后可通过callback指针回调给kuikly,并把context以及src参数回填
 * @return 已处理则返回1,否则返回0
 */
typedef int32_t (*KRImageAdapterV2)(const void *context,
                                 const char *src,
                                 KRSetImageCallback callback);

/**
 * @brief 注册image adapter
 * @param adapter adapter函数指针
 */
void KRRegisterImageAdapterV2(KRImageAdapterV2 adapter);

使用方法

1. 确认CMakeList已链接kuikly_render

如已配置可跳过,链接方法参考上文链接Kuikly业务代码

target_link_libraries(
  ……
  kuikly_render
)


 

2. 头文件引入

在调用 KRRegisterImageAdapterV2 的源文件中增加 include。如在 C++ 目录下的 napi_init.cpp 文件中 include 如下头文件:

#include <Kuikly/Kuikly.h>

3. Adapter实现

// entry/src/main/cpp/napi_init.cpp
#include <Kuikly/Kuikly.h>

static int32_t MyImageAdapter(const void *context, const char *src, KRSetImageCallback callback) {
    // 自定义图片加载逻辑
    // 例如:网络图片下载、本地图片加载等
    
    // 如果已处理该图片加载请求,返回1
    // 否则返回0,让kuikly使用默认处理方式
    return 0;
}

4. Adapter注册

可在使用 Kuikly 前进行 adapter 注册,作为示例,简单起见这里在 InitKuikly 中进行了注册,实际使用时可以在其他更早时机,也应该注意不要多次注册。

// entry/src/main/cpp/napi_init.cpp
static napi_value InitKuikly(napi_env env, napi_callback_info info) {
    KRRegisterImageAdapterV2(MyImageAdapter);
    
    // ...
}

完成后,可通过模版工程中的ImageAdapter基准测试页面来验证功能正常。

提示

鸿蒙端暂不支持capInset能力,请忽略ImageAdapter基准测试中的capInset测试项。

自定义字体适配器示例

该适配器非必须实现, 业务可根据实际使用需求来决定是否实现。

接口是KRRegisterFontAdapter,定义于Kuikly.h

使用方法

1. 确认CMakeList已链接kuikly_render

如已配置可跳过,链接方法参考上文链接Kuikly业务代码

target_link_libraries(
  ……
  kuikly_render
)


 

2. 头文件引入

在调用KRRegisterFontAdapter的源文件中增加include。如在上述C++目录下的napi_init.cpp文件 include 如下头文件。

#include <Kuikly/Kuikly.h>

3. Adapter实现
具体实现代码,请参考源码工程 core-render-ohos/entry 模块的napi_init.cpp类。

// entry/src/main/cpp/napi_init.cpp
...
#include <Kuikly/Kuikly.h>
...
static char *MyFontAdapter(const char *fontFamily, char **fontBuffer, size_t *len, KRFontDataDeallocator *deallocator) {
    if (isEqual(fontFamily, "Satisfy-Regular")) {
        return "rawfile:Satisfy-Regular.ttf";
    }
    return (char *)customFontPath.c_str();
}
...

4. Adapter注册 可在使用Kuikly前进行adapter注册,作为示例,简单起见这里在 InitKuikly 中进行了注册,实际使用的时候可以在其他更早实际,也应该注意不要多次注册。

// entry/src/main/cpp/napi_init.cpp
...
static napi_value InitKuikly(napi_env env, napi_callback_info info) {
    KRRegisterFontAdapter(MyFontAdapter, "Satisfy-Regular");
    
    // ...
 }
 ...

5. 如何获得字体路径

业务一般通过网络等途径下载字体,这种情况下可以通过adapter返回路径即可。 不过有的业务会将字体文件放到rawfile中,但目前还有没有稳定获取rawfile路径的方法,可参考demo片段把字体拷贝到临时文件目录中:

  // copy font data to tmp folder
  const content = getContext().resourceManager.getRawFileContentSync("Satisfy-Regular.ttf")
  const destPath = `${getContext().tempDir}/Satisfy-Regular.ttf`;

  fs.open(destPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE, (err: BusinessError, data) => {
    if (err) {
      console.error("copy file failed with error message: " + err.message + ", error code: " + err.code);
    } else {
      fs.write(data.fd, content.buffer, {offset: 0, length: content.length}).then((result)=>{
        console.info(`copy file succeed:${result}`);
        Napi.setFontPath(destPath)
      })
    }
  })

并通过一个setFontPath接口设置给c++侧,让adapter返回:

此处NAPI调用设置可以参考NAPI初始化逻辑

static std::string customFontPath;

static napi_value SetFontPath(napi_env env, napi_callback_info info) {
    if (customFontPath.size() > 0) {
        return nullptr;
    }

    size_t argc = 1;
    napi_value args[1] = {nullptr};
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    size_t length = 0;
    napi_status status;
    status = napi_get_value_string_utf8(env, args[0], nullptr, 0, &length);
    std::string buffer(length, 0);
    status = napi_get_value_string_utf8(env, args[0], reinterpret_cast<char *>(buffer.data()), length + 1, &length);
    customFontPath = buffer;

    return nullptr;
}

static char *MyFontAdapter(const char *fontFamily, char **fontBuffer, size_t *len, KRFontDataDeallocator *deallocator) {
    if (isEqual(fontFamily, "Satisfy-Regular")) {
        return "rawfile:Satisfy-Regular.ttf";
    }
    return (char *)customFontPath.c_str();
}