Flutter_udid 三方库鸿蒙适配之旅:从零到一的深度实践
Flutter_udid 三方库鸿蒙适配之旅:从零到一的深度实践
在数字化浪潮的推动下,跨平台开发框架如 Flutter 凭借其高效、便捷的特性,成为了开发者们的宠儿。而鸿蒙系统的崛起,更是为跨平台开发注入了新的活力。为了助力开发者在鸿蒙生态中快速实现 Flutter_udid 通知功能,本文将深入浅出地为大家解析如何适配 Flutter_udid 三方库至鸿蒙平台。
一、OAID介绍
开放匿名设备标识符(Open Anonymous Device Identifier, OAID,以下简称OAID):是一种非永久性设备标识符,基于开放匿名设备标识符,OAID是基于华为自有算法生成的32位类UUID(Universally Unique Identifier)标识符,格式为xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx。
OAID的特性:
- OAID是设备级标识符,同一台设备上不同的App获取到的OAID值一样。
- OAID的获取受应用的“跨应用关联访问权限”开关影响:当应用的“跨应用关联访问权限”开关开启时,该应用可获取到非全0的有效OAID;当应用的“跨应用关联访问权限”开关关闭时,该应用仅能获取到全0的OAID。
- 同一台设备上首个应用开启应用“跨应用关联访问权限”开关时,会首次生成OAID
OAID会在下述场景中发生变化:
- 用户恢复手机出厂设置。
- 用户操作重置OAID。
鸿蒙原生开发步骤
在模块的module.json5文件中,申请广告跟踪权限ohos.permission.APP_TRACKING_CONSENT,该权限为user_grant权限,当申请的权限为user_grant权限时,reason,abilities标签必填,配置方式参见requestPermissions标签说明,示例代码如下所示:
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.APP_TRACKING_CONSENT",
        "reason": "$string:reason",
        "usedScene": {
          "abilities": [
            "EntryFormAbility"
          ],
          "when": "inuse"
        }
      }
    ]
  }
}
应用在需要获取OAID信息时,应通过调用requestPermissionsFromUser接口获取对应权限。注意:其中context的获取方式参见各类Context的获取方式。示例代码如下所示:
import { identifier } from '@kit.AdsKit';
import { abilityAccessCtrl, common } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
function requestOAIDTrackingConsentPermissions(context: common.Context): void {
  // 进入页面时,向用户请求授权广告跨应用关联访问权限
  const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  try {
    atManager.requestPermissionsFromUser(context, ["ohos.permission.APP_TRACKING_CONSENT"]).then((data) => {
      if (data.authResults[0] === 0) {
        hilog.info(0x0000, 'testTag', '%{public}s', 'succeeded in requesting permission');
        identifier.getOAID((err: BusinessError, data: string) => {
          if (err.code) {
            hilog.error(0x0000, 'testTag', '%{public}s', `get oaid failed, error: ${err.code} ${err.message}`);
          } else {
            const oaid: string = data;
            hilog.info(0x0000, 'testTag', '%{public}s', `succeeded in getting oaid by callback , oaid: ${oaid}`);
          }
        });
      } else {
        hilog.error(0x0000, 'testTag', '%{public}s', 'user rejected');
      }
    }).catch((err: BusinessError) => {
      hilog.error(0x0000, 'testTag', '%{public}s', `request permission failed, error: ${err.code} ${err.message}`);
    })
  } catch (err) {
    hilog.error(0x0000, 'testTag', '%{public}s', `catch err->${err.code}, ${err.message}`);
  }
}
今天我们一起来看一下如何适配与使用这个三方库
二、flutter_udid介绍
flutter_udid 是一个 Flutter 插件,用于在 iOS、Android、Mac、Windows 和 Linux 平台上检索跨应用重新安装的持久性唯一设备标识符 (UDID)。它的主要特点是提供一种方法来生成和存储设备的唯一标识符,即使在卸载应用后,仍然能保持一致。今天我们会适配支持鸿蒙。
三、插件库使用
适配 OpenHarmony 平台的详细使用指导可以参考:Flutter使用指导文档
在项目中使用该插件库时,只需在 pubspec.yaml 文件的 dependencies 中新增如下配置:
dependencies:
  flutter_udid:
    git:
      url: "https://gitcode.com/nutpi/flutter_udid.git"
      path: ""
然后在项目根目录运行 flutter pub get,即可完成依赖添加
接下来是具体的适配过程。
四、适配过程详解
(一)准备工作
确保已经配置好了 Flutter 开发环境,具体可参考 Flutter 配置指南。同时,从 官方插件库 下载待适配的三方插件。本指导书, 以适配 fflutter_udid4.0.0 为例
(二)插件目录结构
下载并解压插件后,我们会看到以下目录结构:
- lib :对接 Dart 端代码的入口,由此文件接收到参数后,通过 channel 将数据发送到原生端。
- android :安卓端代码实现目录。
- ios :iOS 原生端实现目录。
- example :一个依赖于该插件的 Flutter 应用程序,用于说明如何使用它。
- README.md :介绍包的文件。
- CHANGELOG.md :记录每个版本中的更改。
- LICENSE :包含软件包许可条款的文件。
(三)创建插件的鸿蒙模块
在插件目录下,打开 Terminal,执行以下命令来创建一个鸿蒙平台的 Flutter 模块:
flutter create . --template=plugin --platforms=ohos
步骤:
- 
  用vscode/trae打开刚刚下载好的插件。 
- 
  打开Terminal,cd到插件目录下。 
- 
  执行命令 flutter create . --template=plugin --platforms=ohos创建一个ohos平台的flutter模块。
(四)在根目录下添加鸿蒙平台配置
在项目根目录的 pubspec.yaml 文件中,添加鸿蒙平台的相关配置:
name: flutter_udid
description: Plugin to retrieve a persistent UDID across reinstalls on iOS and Android
version: 4.0.0
homepage: https://github.com/GigaDroid/flutter_udid
environment:
  sdk: ">=3.0.0 <4.0.0"
  flutter: ">=3.10.0"
dependencies:
  flutter:
    sdk: flutter
  crypto: ^3.0.0
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^4.0.0
flutter:
  plugin:
    platforms:
      android:
        package: de.gigadroid.flutter_udid
        pluginClass: FlutterUdidPlugin
      ios:
        pluginClass: FlutterUdidPlugin
      macos:
        pluginClass: FlutterUdidPlugin
      windows:
        pluginClass: FlutterUdidPluginCApi
      linux:
        pluginClass: FlutterUdidPlugin
      ohos:
        pluginClass: FlutterUdidPlugin
(五)编写鸿蒙插件的原生 ETS 模块
1. 创建鸿蒙插件模块
使用 DevEco Studio 打开鸿蒙项目。
2. 修改相关配置文件
在 ohos 目录内的 oh-package.json5 文件中添加 libs/flutter.har 依赖,并创建 .gitignore 文件,添加以下内容以忽略 libs 目录:
/node_modules
/oh_modules
/local.properties
/.preview
/.idea
/build
/libs
/.cxx
/.test
*.har
/BuildProfile.ets
/oh-package-lock.json5
oh-package.json5 文件内容如下:
{
  "name": "flutter_udid",
  "version": "1.0.0",
  "description": "Plugin to retrieve a persistent UDID accross reinstalls on HarmonyOS and OpenHarmony",
  "main": "index.ets",
  "author": "nutpi",
  "license": "Apache-2.0",
  "dependencies": {
    "@ohos/flutter_ohos": "file:./libs/flutter.har"
  },
  "modelVersion": "5.0.4"
}
在 ohos 目录下创建 index.ets 文件,导出配置:
import FlutterUdidPlugin from './src/main/ets/de/gigadroid/flutter_udid/FlutterUdidPlugin'
export default FlutterUdidPlugin
3. 编写 ETS 代码
文件结构和代码逻辑可以参考安卓或 iOS 的实现,鸿蒙的 API 文档可以参考 :https://gitcode.com/openharmony-sig/flutter_packages/tree/master/packages/path_provider/path_provider_android
ohos的api可以参考:https://gitcode.com/openharmony/docs
以下是 FlutterUdidPlugin.ets 文件的代码示例:
import {
  MethodCallHandler,
  FlutterPlugin,
  BinaryMessenger,
  FlutterPluginBinding,
  AbilityPluginBinding,
  MethodCall,
  MethodChannel,
  MethodResult,
  Log
} from '@ohos/flutter_ohos';
import {  common } from '@kit.AbilityKit';
import { identifier } from '@kit.AdsKit';
import { UIAbility } from '@kit.AbilityKit';
import { requestPermissions } from './RequestPermissions';
const TAG: string = "FlutterUdidPlugin";
export default class FlutterUdidPlugin implements MethodCallHandler, FlutterPlugin {
  private channel: MethodChannel | null = null
  private applicationContext: common.Context | null = null
  private ability: UIAbility | null = null;
  constructor(context?: common.Context) {
    if (context) {
      this.applicationContext = context;
    }
  }
  static registerWith(): void {
  }
  getUniqueClassName(): string {
    return TAG;
  }
  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.onAttachedToEngine1(binding.getApplicationContext(), binding.getBinaryMessenger());
  }
  private onAttachedToEngine1(applicationContext: Context, messenger: BinaryMessenger) {
    this.applicationContext = applicationContext;
    this.channel = new MethodChannel(messenger, "flutter_udid")
    this.channel.setMethodCallHandler(this)
  }
  onAttachedToAbility(binding: AbilityPluginBinding): void {
    this.ability = binding.getAbility()
  }
  onDetachedFromAbility(): void {
    this.ability = null;
  }
  onMethodCall(call: MethodCall, result: MethodResult): void {
    if (call.method == "getUDID") {
      this.requestPermissions().then((data: boolean) => {
        this.getUDID().then((udid: string | null) => {
          if (udid == null || udid == "") {
            result.error("UNAVAILABLE", "UDID not available.", null)
          } else {
            result.success(udid)
          }
        })
      })
    } else {
      result.notImplemented()
    }
  }
  onDetachedFromEngine(binding: FlutterPluginBinding): void {
    this.applicationContext = null;
    this.channel?.setMethodCallHandler(null)
  }
  private async getUDID(): Promise<string | null> {
    try {
      return await identifier.getOAID()
    } catch (err) {
      return null
    }
  }
  private async requestPermissions(): Promise<boolean> {
    if (!this.ability) {
      Log.i(TAG, "Could not launch BarcodeScanner because the plugin is not attached to any ability")
      return false
    }
    try {
      const results = await requestPermissions(this.ability!.context)
      return results ? results : false
    } catch (e) {
    }
    return false
  }
}
以下是 requestPermissions.ets 文件的代码示例:
import bundleManager from '@ohos.bundle.bundleManager';
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';
import { BusinessError } from '@kit.BasicServicesKit';
export const appTrackingConsentPermission: Array<Permissions> = ['ohos.permission.APP_TRACKING_CONSENT'];
async function checkAccessToken(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
  let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  let grantStatus: abilityAccessCtrl.GrantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;
  // 获取应用程序的accessTokenID
  let tokenId: number = 0;
  try {
    let bundleInfo: bundleManager.BundleInfo =
      await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
    let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
    tokenId = appInfo.accessTokenId;
  } catch (error) {
    let err: BusinessError = error as BusinessError;
    console.error(`Failed to get bundle info for self. Code is ${err.code}, message is ${err.message}`);
  }
  // 校验应用是否被授予权限
  try {
    grantStatus = await atManager.checkAccessToken(tokenId, permission);
  } catch (error) {
    let err: BusinessError = error as BusinessError;
    console.error(`Failed to check access token. Code is ${err.code}, message is ${err.message}`);
  }
  return grantStatus;
}
export async function checkPermissions(permissions: Array<Permissions>): Promise<boolean> {
  let grantStatus: abilityAccessCtrl.GrantStatus = await checkAccessToken(permissions[0]);
  return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
}
export async function requestPermissions(
  context: Context
) {
  const hasAppTrackingConsentPermission: boolean = await checkPermissions(appTrackingConsentPermission);
  if (!hasAppTrackingConsentPermission) {
    let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
    try {
      let data = await atManager.requestPermissionsFromUser(context, appTrackingConsentPermission)
      let grantStatus: Array<number> = data.authResults;
      let length: number = grantStatus.length;
      for (let i = 0; i < length; i++) {
        if (grantStatus[i] !== 0) {
          // 用户拒绝授权
          return false;
        }
      }
      // 用户授权,可以继续访问目标操作
      return true;
    } catch (error) {
      let err: BusinessError = error as BusinessError;
      console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
      return false;
    }
  } else {
    // Permissions already exist. Call the callback with success.
    return true;
  }
}
4. 修改 index 文件
在 ohos 目录下创建 index.ets 文件,导出 FlutterToastPlugin:
import FlutterUdidPlugin from './src/main/ets/de/gigadroid/flutter_udid/FlutterUdidPlugin'
export default FlutterUdidPlugin
(六)编写 Example
1. 创建 Example 应用
在最开始使用
flutter create . --template=plugin --platforms=ohos
创建模版的时候,也会配套创建Example
2. 签名与运行
使用 Deveco Studio 打开 example > ohos 目录,单击 File > Project Structure > Project > Signing Configs,勾选 Automatically generate signature,等待自动签名完成。然后运行以下命令:
flutter pub get
 flutter build hap --debug
如果应用正常启动,说明插件适配成功。如果没有,欢迎大家联系坚果派一起支持。
五、总结
通过以上步骤,我们成功地将 Flutterudid 三方库适配到了鸿蒙平台。这个过程涉及到了解插件的基本信息、配置开发环境、创建鸿蒙模块、编写原生代码以及测试验证等多个环节。希望这篇博客能够帮助到需要进行 FlutterToast 鸿蒙适配的开发者们,让大家在鸿蒙生态的开发中更加得心应手。
六、参考
- [如何使用Flutter与OpenHarmony通信 FlutterChannel](https://gitcode.com/openharmony-sig/flutter_samples/blob/master/ohos/docs/04_development/如何使用Flutter与OpenHarmony通信 FlutterChannel.md)
- [开发module](https://gitcode.com/openharmony-sig/flutter_samples/blob/master/ohos/docs/04_development/如何使用混合开发 module.md)
- 开发package
- 开发plugin
- [开发FFI plugin](https://gitcode.com/openharmony-sig/flutter_samples/blob/master/ohos/docs/04_development/开发FFI plugin.md)
- https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V14/oaid-service-V14
- https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/reference/apis-ads-kit/js-apis-oaid.md
七、坚果派
坚果派由坚果等人创建,团队拥有若干华为HDE,以及若干其他领域的三十余位万粉博主运营。专注于分享的技术包括HarmonyOS/OpenHarmony,ArkUI-X,元服务,服务卡片,华为自研语言,BlueOS操作系统、团队成员聚集在北京、上海、广州、深圳、南京、杭州、苏州、宁夏等地。 聚焦“鸿蒙原生应用”、“智能物联”和“AI赋能”、“人工智能”四大业务领域,依托华为开发者专家等强大的技术团队,以及涵盖需求、开发、测试、运维于一体的综合服务体系,赋能文旅、媒体、社交、家居、消费电子等行业客户,满足社区客户数字化升级转型的需求,帮助客户实现价值提升。 目前上架鸿蒙原生应用18款,三方库72个。
八、鸿蒙 Flutter 插件:获取 OAID 与应用跟踪权限处理
本文档旨在详细说明在一个 Flutter 插件中,鸿蒙(HarmonyOS)原生侧如何处理获取 OAID(开放匿名设备标识符)时所需的应用跟踪权限(ohos.permission.APP_TRACKING_CONSENT)。我们将基于 FlutterUdidPlugin.ets 和 RequestPermissions.ets 这两个关键文件进行分析。
在许多场景下,应用需要一个设备标识符来进行数据分析、广告投放等。在鸿蒙系统中,OAID 是推荐使用的非永久性、可重置的设备标识符。然而,获取 OAID 需要用户明确授予“应用跟踪”权限。
我们的 Flutter 插件 flutter_udid 旨在提供一个跨平台的接口来获取设备标识符。在鸿蒙侧,这意味着我们需要:
- 检查应用是否已被授予 ohos.permission.APP_TRACKING_CONSENT权限。
- 如果未授权,向用户请求该权限。
- 在获得权限后,调用鸿蒙提供的 API 获取 OAID。
权限处理核心:RequestPermissions.ets
RequestPermissions.ets 文件封装了检查和请求 ohos.permission.APP_TRACKING_CONSENT 权限的逻辑。其核心功能分为两部分:检查权限和请求权限。
1. 检查权限 (checkPermissions 和 checkAccessToken)
- checkPermissions(permissions: Array<Permissions>): Promise<boolean>: 这是暴露给外部调用的函数,用于检查传入的权限列表(在此场景下主要是- appTrackingConsentPermission)是否已被授予。
- checkAccessToken(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus>: 这是实际执行权限检查的内部函数。它执行以下步骤:- 使用 abilityAccessCtrl.createAtManager()创建权限管理实例。
- 使用 bundleManager.getBundleInfoForSelf()获取当前应用的tokenId(accessTokenId)。这是检查特定应用权限所必需的。
- 调用 atManager.checkAccessToken(tokenId, permission)来检查指定tokenId的应用是否已被授予permission。
- 返回权限的授予状态 (GrantStatus)。
 
- 使用 
- checkPermissions函数最终会比较- checkAccessToken返回的状态是否为- PERMISSION_GRANTED,并返回一个布尔值。
// RequestPermissions.ets (简化示例)
import bundleManager from '@ohos.bundle.bundleManager';
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';
export const appTrackingConsentPermission: Array<Permissions> = ['ohos.permission.APP_TRACKING_CONSENT'];
async function checkAccessToken(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
  // ... 获取 tokenId
  // ... 调用 atManager.checkAccessToken(tokenId, permission)
  // ... 返回 grantStatus
}
export async function checkPermissions(permissions: Array<Permissions>): Promise<boolean> {
  let grantStatus: abilityAccessCtrl.GrantStatus = await checkAccessToken(permissions[0]);
  return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
}
2. 请求权限 (requestPermissions)
- requestPermissions(context: Context): Promise<boolean>: 这是主要的权限请求函数。- 首先调用 checkPermissions检查是否已获得appTrackingConsentPermission。
- 如果已授权:直接返回 true。
- 如果未授权: 
    - 创建 abilityAccessCtrl.AtManager实例。
- 调用 atManager.requestPermissionsFromUser(context, appTrackingConsentPermission)。这个方法会触发系统弹窗,请求用户授权。
- requestPermissionsFromUser返回一个包含授权结果(- authResults)的对象。这是一个数字数组,- 0代表授予,非- 0代表拒绝。
- 遍历 authResults,如果所有请求的权限都被授予(即所有结果都是0),则返回true;否则返回false。
- 包含错误处理逻辑,捕获请求过程中可能出现的 BusinessError。
 
- 创建 
 
- 首先调用 
// RequestPermissions.ets (简化示例)
export async function requestPermissions(
  context: Context
): Promise<boolean> {
  const hasPermission: boolean = await checkPermissions(appTrackingConsentPermission);
  if (!hasPermission) {
    let atManager = abilityAccessCtrl.createAtManager();
    try {
      let data = await atManager.requestPermissionsFromUser(context, appTrackingConsentPermission);
      // ... 检查 data.authResults ...
      // ... 返回 true 或 false
    } catch (error) {
      // ... 处理错误,返回 false
    }
  } else {
    return true;
  }
}
插件集成:FlutterUdidPlugin.ets
FlutterUdidPlugin.ets 是 Flutter 与鸿蒙原生代码交互的桥梁。它通过 MethodChannel 接收来自 Flutter 端的调用。
当 Flutter 端调用 getUDID 方法时,onMethodCall 函数会被触发:
- 调用权限请求: 首先调用 this.requestPermissions()。注意,这个requestPermissions是FlutterUdidPlugin类内部的方法,它会进一步调用我们之前分析的RequestPermissions.ets中的requestPermissions函数。- FlutterUdidPlugin内部的- requestPermissions方法需要确保- this.ability(当前的 UIAbility 实例) 不为- null,因为请求权限需要一个- Context,通常从- Ability获取。
- 它调用 RequestPermissions.ets中的requestPermissions(this.ability!.context)。
 
- 处理权限结果: this.requestPermissions()返回一个 Promise,其结果是一个布尔值data,表示权限是否被授予。
- 获取 OAID: 在权限请求的 Promise 完成后(.then((data: boolean) => { ... })),无论权限是否成功授予(虽然逻辑上应该只在授予后获取,但当前代码结构是在 then 回调中直接调用getUDID),都会调用this.getUDID()。- getUDID()方法使用- @kit.AdsKit中的- identifier.getOAID()来异步获取 OAID。
 
- 返回结果给 Flutter: 获取 OAID 的 Promise 完成后 (.then((udid: string | null) => { ... })):- 如果 udid为null或空字符串,通过result.error()返回错误给 Flutter 端。
- 如果成功获取 udid,通过result.success(udid)将 OAID 返回给 Flutter 端。
 
- 如果 
// FlutterUdidPlugin.ets (简化示例)
import { requestPermissions as requestAppPermissions } from './RequestPermissions';
import { identifier } from '@kit.AdsKit';
export default class FlutterUdidPlugin implements MethodCallHandler, FlutterPlugin {
  // ... 其他成员和方法 ...
  private ability: UIAbility | null = null;
  onMethodCall(call: MethodCall, result: MethodResult): void {
    if (call.method == "getUDID") {
      this.requestPermissions().then((granted: boolean) => {
        // 注意:原始代码在这里没有显式检查 granted 的值
        // 理想情况下,应该检查 granted 是否为 true
        // if (granted) { ... } else { result.error(...) }
        this.getUDID().then((udid: string | null) => {
          if (udid) {
            result.success(udid);
          } else {
            result.error("UNAVAILABLE", "UDID not available.", null);
          }
        });
      });
    } else {
      result.notImplemented();
    }
  }
  private async getUDID(): Promise<string | null> {
    try {
      return await identifier.getOAID();
    } catch (err) {
      return null;
    }
  }
  private async requestPermissions(): Promise<boolean> {
    if (!this.ability) {
      return false;
    }
    try {
      // 调用 RequestPermissions.ets 中的函数
      return await requestAppPermissions(this.ability!.context);
    } catch (e) {
      return false;
    }
  }
}
结论
通过 RequestPermissions.ets 封装鸿蒙的权限检查和请求逻辑,FlutterUdidPlugin.ets 可以在需要时调用这些功能,确保在获取 OAID 之前,应用已经获得了用户的应用跟踪许可。这种分层设计使得权限管理逻辑清晰、可复用,并简化了主插件逻辑。
注意: 示例代码中的 onMethodCall 在调用 getUDID 前并未严格检查 requestPermissions 的返回结果。在实际应用中,建议在确认权限被授予 (granted === true) 后才执行获取 OAID 的操作,并在权限被拒绝时向 Flutter 返回相应的错误信息。
命令
flutter pub get
 flutter build hap --debug
flutter config --ohos-sdk=/Users/jianguo/Library/OpenHarmony/Sdk
flutter config --ohos-sdk=""
- 17回答
- 28粉丝
- 12关注
- FlutterToast 三方库鸿蒙适配之旅:从零到一的深度实践
- flutter_exit_app 三方库鸿蒙适配之旅:从零到一的深度实践
- 从零到一:flutter_timezone库的鸿蒙适配深度探索
- 打造鸿蒙三方库生态新基石:从适配共建到生态繁荣
- Flutter到鸿蒙的跨越:torch_light库的鸿蒙适配之旅
- Flutter到鸿蒙的跨越:flutter_native_contact_picker库的鸿蒙适配之旅
- Flutter到鸿蒙的跨越:flutter_native_contact_picker库的鸿蒙适配之旅
- 鸿蒙--如何发布一个三方库
- 童长老的三方库
- 上传PR到第三方库可能遇到的问题
- 童长老的三方开源库
- [童长老的三方开源库]
- 童长老的三方开源库
- 童长老的三方开源库
- 童长老的三方开源库

