Flutter到鸿蒙的跨越:flutter_native_contact_picker库的鸿蒙适配之旅

2025-04-21 18:35:21
133次阅读
0个评论
最后修改时间:2025-04-22 08:03:38

Flutter到鸿蒙的跨越:flutter_native_contact_picker库的鸿蒙适配之旅

本项目作者:王阳科/坚果

适配仓库地址

作者仓库:https://github.com/jayeshpansheriya/flutter_native_contact_picker

在数字化浪潮的推动下,跨平台开发框架如 Flutter 凭借其高效、便捷的特性,成为了开发者们的宠儿。而鸿蒙系统的崛起,更是为跨平台开发注入了新的活力。为了助力开发者在鸿蒙生态中快速实现 flutter_native_contact_picker联系人选择功能,本文将深入浅出地为大家解析如何适配 flutter_native_contact_picker 三方库至鸿蒙平台。

一、适配鸿蒙版 flutter_native_contact_picker 三方库

(一)版本选择与仓库简介

我们先去 pub 上查看最新版本,我们选择以 0.0.10版本为基础进行适配。flutter_native_contact_picker 是一个用于在 Flutter 应用中选择联系人的插件,其 GitHub 仓库为https://github.com/jayeshpansheriya/flutter_native_contact_picker ,我们的目标是将这个插件适配到鸿蒙平台。

(二)引入背景与使用场景

在 OpenHarmony 北向生态的发展过程中,许多已经适配了 Flutter 的厂商在接入 OpenHarmony 时,都希望能够继续使用 FlutterToast 来实现通知功能。因此,我们提供了这个适配方案,采用插件化的适配器模式,帮助生态伙伴快速实现产品化。

本方案适用于已经支持 Flutter 框架的设备在移植到 OpenHarmony 系统过程中,作为一个备选方案。

(三)使用文档与插件库使用

适配 OpenHarmony 平台的详细使用指导可以参考:Flutter使用指导文档

在项目中使用该插件库时,只需在 pubspec.yaml 文件的 dependencies 中新增如下配置:

dependencies:
  flutter_native_contact_picker:
    git:
      url: "https://gitcode.com/nutpi/flutter_native_contact_picker.git"
      path: ""

然后在项目根目录运行 flutter pub get,即可完成依赖添加

接下来是具体的适配过程。

二、适配过程详解

(一)准备工作

确保已经配置好了 Flutter 开发环境,具体可参考 Flutter 配置指南。同时,从 官方插件库 下载待适配的三方插件。本指导书, 以适配 flutter_native_contact_picker 为例

image-20250417200546042

(二)插件目录结构

下载并解压插件后,我们会看到以下目录结构:

  • lib :对接 Dart 端代码的入口,由此文件接收到参数后,通过 channel 将数据发送到原生端。
  • android :安卓端代码实现目录。
  • ios :iOS 原生端实现目录。
  • example :一个依赖于该插件的 Flutter 应用程序,用于说明如何使用它。
  • README.md :介绍包的文件。
  • CHANGELOG.md :记录每个版本中的更改。
  • LICENSE :包含软件包许可条款的文件。

(三)创建插件的鸿蒙模块

在插件目录下,打开 Terminal,执行以下命令来创建一个鸿蒙平台的 Flutter 模块:

flutter create . --template=plugin --platforms=ohos

步骤:

  1. 用vscode/trae打开刚刚下载好的插件。

  2. 打开Terminal,cd到插件目录下。

  3. 执行命令flutter create . --template=plugin --platforms=ohos 创建一个ohos平台的flutter模块。

第一个问题,修改sdk的版本,适配旧版本。

我们做好修改就好。

(四)在根目录下添加鸿蒙平台配置

在项目根目录的 pubspec.yaml 文件中,添加鸿蒙平台的相关配置:

name: flutter_native_contact_picker
description: "A Flutter plugin for picking a contact from the address book."
version: 0.0.10
#author: Jayesh Pansheriya <pansheriyajayesh@gmail.com>
homepage: https://github.com/jayeshpansheriya/flutter_native_contact_picker

environment:
  sdk: ^3.4.0
  flutter: '>=3.3.0'

dependencies:
  flutter:
    sdk: flutter
  plugin_platform_interface: ^2.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^4.0.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:
  # This section identifies this Flutter project as a plugin project.
  # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
  # which should be registered in the plugin registry. This is required for
  # using method channels.
  # The Android 'package' specifies package in which the registered class is.
  # This is required for using method channels on Android.
  # The 'ffiPlugin' specifies that native code should be built and bundled.
  # This is required for using `dart:ffi`.
  # All these are used by the tooling to maintain consistency when
  # adding or updating assets for this project.
  plugin:
    platforms:
      android:
        package: com.jayesh.flutter_native_contact_picker
        pluginClass: FlutterNativeContactPickerPlugin
      ios:
        pluginClass: FlutterNativeContactPickerPlugin
      ohos:
        pluginClass: FlutterNativeContactPickerPlugin

  # To add assets to your plugin package, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg
  #
  # For details regarding assets in packages, see
  # https://flutter.dev/to/asset-from-package
  #
  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/to/resolution-aware-images

  # To add custom fonts to your plugin package, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts in packages, see
  # https://flutter.dev/to/font-from-package

(五)编写鸿蒙插件的原生 ArkTS模块

1. 创建鸿蒙插件模块

使用 DevEco Studio 打开鸿蒙项目。

2. 修改相关配置文件

ohos 目录内的 oh-package.json5 文件中添加 libs/flutter.har 依赖,并创建 .gitignore 文件,添加以下内容以忽略 libs 目录:

/node_modules
/oh_modules
/local.properties
/.preview
/.idea
/build
/libs
*.har
/.cxx
/.test
/BuildProfile.ets
/oh-package-lock.json5

oh-package.json5 文件内容如下:

{
  "name": "flutter_native_contact_picker",
  "version": "1.0.0",
  "description": "A Flutter plugin for picking a contact from the address book.",
  "main": "index.ets",
  "author": "nutpi",
  "license": "Apache-2.0",
  "dependencies": {
    "@ohos/flutter_ohos": "file:har/flutter.har"
  }
}

ohos 目录下创建 index.ets 文件,导出配置:


import FlutterNativeContactPickerPlugin from './src/main/ets/components/plugin/FlutterNativeContactPickerPlugin';
export default FlutterNativeContactPickerPlugin;

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

以下是 FlutterNativeContactPickerPlugin.ets 文件的代码示例:

import {
  FlutterPlugin,
  FlutterPluginBinding,
  MethodCall,
  MethodCallHandler,
  MethodChannel,
  MethodResult,
} from '@ohos/flutter_ohos';
import { contact } from '@kit.ContactsKit';
import { BusinessError } from '@kit.BasicServicesKit';

/** FlutterNativeContactPickerPlugin **/
export default class FlutterNativeContactPickerPlugin implements FlutterPlugin, MethodCallHandler {
  private channel: MethodChannel | null = null;

  constructor() {
  }

  getUniqueClassName(): string {
    return "FlutterNativeContactPickerPlugin"
  }

  onAttachedToEngine(binding: FlutterPluginBinding): void {
    console.info(" onAttachedToEngine 被调用,开始初始化插件");
    this.channel = new MethodChannel(binding.getBinaryMessenger(), "flutter_native_contact_picker");
    console.info(" MethodChannel 已创建,通道名称: flutter_native_contact_picker");
    this.channel.setMethodCallHandler(this);
    console.info(" MethodCallHandler 已设置,插件初始化完成");
  }

  onDetachedFromEngine(binding: FlutterPluginBinding): void {
    if (this.channel != null) {
      this.channel.setMethodCallHandler(null)
    }
  }

  onMethodCall(call: MethodCall, result: MethodResult): void {
    console.info(` onMethodCall 被调用,方法名: ${call.method}`);
    // 获取参数,使用正确的属性名 argument 而不是 arguments
    const argument = call.argument || {};

    if (call.method == "getPlatformVersion") {
      console.info(" getPlatformVersion 被调用");
      result.success("OpenHarmony ^ ^ ")
    } else if (call.method == "selectContact") {
      console.info(" selectContact 被调用");
      contact.selectContacts({
        isMultiSelect: false
      }, (err: BusinessError, data) => {
        if (err) {
          console.error(`Failed to select Contacts. Code: ${err.code}, message: ${err.message}`);
          return;
        }
        const contactInfo = new Map<string, string | string[]>();
        if (data && data.length > 0) {
          const contact: contact.Contact = data[0];

          contactInfo.set("fullName", contact.name?.fullName || "");
          const phoneNumbers: string[] = contact.phoneNumbers?.map(item => item.phoneNumber) || [];
          contactInfo.set("phoneNumbers", phoneNumbers);

          if (contact.emails && contact.emails.length > 0) {
            const emails: string[] = contact.emails.map(item => item.email);
            contactInfo.set("emails", emails);
          }
        }
        let jsonObject: Record<string, Object> = {};
        contactInfo.forEach((value, key) => {
          if (key !== undefined && value !== undefined) {
            jsonObject[key] = value;
          }
        })
        console.info(`Succeeded in selecting Contacts. data->${JSON.stringify(jsonObject)}`);
        result.success(jsonObject)
      });
    } else if (call.method == "selectContacts") {
      console.info(" selectContacts 被调用");
      contact.selectContacts({
        isMultiSelect: true
      }, (err: BusinessError, data) => {
        if (err) {
          console.error(`Failed to select Contacts. Code: ${err.code}, message: ${err.message}`);
          result.error(err.code.toString(), err.message, null);
          return;
        }
        console.info(`Succeeded in selecting Contacts. data->${JSON.stringify(data)}`);
        const selectedContacts: contact.Contact[] = [];
        if (data && data.length > 0) {
          for (const contact of data) {
            const contactInfo = new Map<string, string | string[]>();
            contactInfo.set("fullName", contact.name?.fullName || "");
            const phoneNumbers: string[] = contact.phoneNumbers?.map(item => item.phoneNumber) || [];
            contactInfo.set("phoneNumbers", phoneNumbers);

            if (contact.emails && contact.emails.length > 0) {
              const emails: string[] = contact.emails.map(item => item.email);
              contactInfo.set("emails", emails);
            }
            let jsonObject: Record<string, Object> = {};
            contactInfo.forEach((value, key) => {
              if (key !== undefined && value !== undefined) {
                jsonObject[key] = value;
              }
            });
            selectedContacts.push(jsonObject);
          }
        }
        console.info(`Succeeded in selecting Contacts. data->${JSON.stringify(selectedContacts)}`);
        result.success(selectedContacts);
      });


    } else {
      result.notImplemented();
    }
  }
}

这里我主要参考的是

三、Contacts Kit

Contacts Kit可以帮助开发者轻松实现联系人的增删改查等功能。该Kit提供了一系列API,可以让开发者在应用中快速集成联系人管理功能。

详情请参考@ohos.contact API。

使用示例。

contact.selectContacts({
  isMultiSelect:false
},(err: BusinessError, data) => {
    if (err) {
      console.error(`selectContact callback: err->${JSON.stringify(err)}`);
        return;
    }
    console.log(`selectContact callback: success data->${JSON.stringify(data)}`);
});

参数:

参数名 类型 必填 说明
options ContactSelectionOptions 选择联系人时的筛选条件。
callback AsyncCallback<Array<Contact>> 回调函数。成功返回选择的联系人对象数组;失败返回失败的错误码。
let myContact: contact.Contact = {
    phoneNumbers: [{
        phoneNumber: "138xxxxxxxx"
    }],
    name: {
        fullName: "fullName",
        namePrefix: "namePrefix"
    },
    nickName: {
        nickName: "nickName"
    }
};

1.鸿蒙侧选择联系人

import { BusinessError } from '@kit.BasicServicesKit';
contact.selectContacts({
  isMultiSelect:false
}, (err: BusinessError, data) => {
    if (err) {
        console.error(`Failed to select Contacts. Code: ${err.code}, message: ${err.message}`);
        return;
    }
    console.info(`Succeeded in selecting Contacts. data->${JSON.stringify(data)}`);
});

可以使用 ai 直接将鸿蒙的 interface 转换成 dart 的类,并且增加 toMapfromMap,和注释。

2,明确contact返回的数据类似

其中data返回的格式是

[
  {
    "id": 1,
    "key": "1",
    "emails": [],
    "events": [],
    "groups": [],
    "imAddresses": [],
    "phoneNumbers": [
      {
        "phoneNumber": "17752170152",
        "labelName": "手机",
        "labelId": 1
      }
    ],
    "portrait": {
      "uri": ""
    },
    "postalAddresses": [],
    "relations": [],
    "websites": [],
    "name": {
      "fullName": "坚果"
    },
    "note": {
      "noteContent": ""
    },
    "organization": {
      "name": ""
    }
  }
]

3.dart端是这样定义的

class Contact {
  final String? fullName;           // Contact's full name
  final List<String>? phoneNumbers; // All phone numbers (iOS: all numbers, Android: selected number only)
  final String? selectedPhoneNumber; // The specifically selected phone number when using selectPhoneNumber()
}

4.ios侧处理

类似ios侧处理这边的Contact,并返回

 var data = Dictionary<String, Any>()
        data["fullName"] = CNContactFormatter.string(from: contact, style: CNContactFormatterStyle.fullName)
        let numbers: Array<String> = contact.phoneNumbers.compactMap { $0.value.stringValue as String }
        data["phoneNumbers"] = numbers
        result(data)

5.ArkTS处理单选联系人

  console.info(" selectContact 被调用");
      contact.selectContacts({
        isMultiSelect: false
      }, (err: BusinessError, data) => {
        if (err) {
          console.error(`Failed to select Contacts. Code: ${err.code}, message: ${err.message}`);
          return;
        }
        const contactInfo = new Map<string, string | string[]>();
        if (data && data.length > 0) {
          const contact: contact.Contact = data[0];

          contactInfo.set("fullName", contact.name?.fullName || "");
          const phoneNumbers: string[] = contact.phoneNumbers?.map(item => item.phoneNumber) || [];
          contactInfo.set("phoneNumbers", phoneNumbers);

          if (contact.emails && contact.emails.length > 0) {
            const emails: string[] = contact.emails.map(item => item.email);
            contactInfo.set("emails", emails);
          }
        }
        let jsonObject: Record<string, Object> = {};
        contactInfo.forEach((value, key) => {
          if (key !== undefined && value !== undefined) {
            jsonObject[key] = value;
          }
        })
        console.info(`Succeeded in selecting Contacts. data->${JSON.stringify(jsonObject)}`);
        result.success(jsonObject)
      });

6.ArkTS处理多选联系人

多选的处理方式

  console.info(" selectContacts 被调用");
      contact.selectContacts({
        isMultiSelect: true
      }, (err: BusinessError, data) => {
        if (err) {
          console.error(`Failed to select Contacts. Code: ${err.code}, message: ${err.message}`);
          result.error(err.code.toString(), err.message, null);
          return;
        }
        console.info(`Succeeded in selecting Contacts. data->${JSON.stringify(data)}`);
        const selectedContacts: contact.Contact[] = [];
        if (data && data.length > 0) {
          for (const contact of data) {
            const contactInfo = new Map<string, string | string[]>();
            contactInfo.set("fullName", contact.name?.fullName || "");
            const phoneNumbers: string[] = contact.phoneNumbers?.map(item => item.phoneNumber) || [];
            contactInfo.set("phoneNumbers", phoneNumbers);

            if (contact.emails && contact.emails.length > 0) {
              const emails: string[] = contact.emails.map(item => item.email);
              contactInfo.set("emails", emails);
            }
            let jsonObject: Record<string, Object> = {};
            contactInfo.forEach((value, key) => {
              if (key !== undefined && value !== undefined) {
                jsonObject[key] = value;
              }
            });
            selectedContacts.push(jsonObject);
          }
        }
        console.info(`Succeeded in selecting Contacts. data->${JSON.stringify(selectedContacts)}`);
        result.success(selectedContacts);
      });

7.完整的代码

import {
  FlutterPlugin,
  FlutterPluginBinding,
  MethodCall,
  MethodCallHandler,
  MethodChannel,
  MethodResult,
} from '@ohos/flutter_ohos';
import { contact } from '@kit.ContactsKit';
import { BusinessError } from '@kit.BasicServicesKit';

/** FlutterNativeContactPickerPlugin **/
export default class FlutterNativeContactPickerPlugin implements FlutterPlugin, MethodCallHandler {
  private channel: MethodChannel | null = null;

  constructor() {
  }

  getUniqueClassName(): string {
    return "FlutterNativeContactPickerPlugin"
  }

  onAttachedToEngine(binding: FlutterPluginBinding): void {
    console.info(" onAttachedToEngine 被调用,开始初始化插件");
    this.channel = new MethodChannel(binding.getBinaryMessenger(), "flutter_native_contact_picker");
    console.info(" MethodChannel 已创建,通道名称: flutter_native_contact_picker");
    this.channel.setMethodCallHandler(this);
    console.info(" MethodCallHandler 已设置,插件初始化完成");
  }

  onDetachedFromEngine(binding: FlutterPluginBinding): void {
    if (this.channel != null) {
      this.channel.setMethodCallHandler(null)
    }
  }

  onMethodCall(call: MethodCall, result: MethodResult): void {
    console.info(` onMethodCall 被调用,方法名: ${call.method}`);
    // 获取参数,使用正确的属性名 argument 而不是 arguments
    const argument = call.argument || {};

    if (call.method == "getPlatformVersion") {
      console.info(" getPlatformVersion 被调用");
      result.success("OpenHarmony ^ ^ ")
    } else if (call.method == "selectContact") {
      console.info(" selectContact 被调用");
      contact.selectContacts({
        isMultiSelect: false
      }, (err: BusinessError, data) => {
        if (err) {
          console.error(`Failed to select Contacts. Code: ${err.code}, message: ${err.message}`);
          return;
        }
        const contactInfo = new Map<string, string | string[]>();
        if (data && data.length > 0) {
          const contact: contact.Contact = data[0];

          contactInfo.set("fullName", contact.name?.fullName || "");
          const phoneNumbers: string[] = contact.phoneNumbers?.map(item => item.phoneNumber) || [];
          contactInfo.set("phoneNumbers", phoneNumbers);

          if (contact.emails && contact.emails.length > 0) {
            const emails: string[] = contact.emails.map(item => item.email);
            contactInfo.set("emails", emails);
          }
        }
        let jsonObject: Record<string, Object> = {};
        contactInfo.forEach((value, key) => {
          if (key !== undefined && value !== undefined) {
            jsonObject[key] = value;
          }
        })
        console.info(`Succeeded in selecting Contacts. data->${JSON.stringify(jsonObject)}`);
        result.success(jsonObject)
      });
    } else if (call.method == "selectContacts") {
      console.info(" selectContacts 被调用");
      contact.selectContacts({
        isMultiSelect: true
      }, (err: BusinessError, data) => {
        if (err) {
          console.error(`Failed to select Contacts. Code: ${err.code}, message: ${err.message}`);
          result.error(err.code.toString(), err.message, null);
          return;
        }
        console.info(`Succeeded in selecting Contacts. data->${JSON.stringify(data)}`);
        const selectedContacts: contact.Contact[] = [];
        if (data && data.length > 0) {
          for (const contact of data) {
            const contactInfo = new Map<string, string | string[]>();
            contactInfo.set("fullName", contact.name?.fullName || "");
            const phoneNumbers: string[] = contact.phoneNumbers?.map(item => item.phoneNumber) || [];
            contactInfo.set("phoneNumbers", phoneNumbers);

            if (contact.emails && contact.emails.length > 0) {
              const emails: string[] = contact.emails.map(item => item.email);
              contactInfo.set("emails", emails);
            }
            let jsonObject: Record<string, Object> = {};
            contactInfo.forEach((value, key) => {
              if (key !== undefined && value !== undefined) {
                jsonObject[key] = value;
              }
            });
            selectedContacts.push(jsonObject);
          }
        }
        console.info(`Succeeded in selecting Contacts. data->${JSON.stringify(selectedContacts)}`);
        result.success(selectedContacts);
      });


    } else {
      result.notImplemented();
    }
  }
}

参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-inner-application-uiabilitycontext#uiabilitycontextterminateself-1和https://gitcode.com/openharmony-sig/flutter_contacts/tree/master/ohos/src/main/ets/components

四、编写 Example

1. 创建 Example 应用

在插件根目录下创建一个名为 example 的文件夹,用于存放示例应用。在 example 文件夹中,创建一个鸿蒙平台的 Flutter 应用,用于验证插件功能。

2. 签名与运行

使用 Deveco Studio 打开 example > ohos 目录,单击 File > Project Structure > Project > Signing Configs,勾选 Automatically generate signature,等待自动签名完成。然后运行以下命令:

flutter pub get

 flutter build hap --debug

如果应用正常启动,说明插件适配成功。如果没有,欢迎大家联系坚果派一起支持。

五、总结

通过以上步骤,我们成功地将 flutter_native_contact_picker 三方库适配到了鸿蒙平台。这个过程涉及到了解插件的基本信息、配置开发环境、创建鸿蒙模块、编写原生代码以及测试验证等多个环节。希望这篇博客能够帮助到需要进行 flutter_native_contact_picker 鸿蒙适配的开发者们,让大家在鸿蒙生态的开发中更加得心应手。

六、参考

七、坚果派

坚果派由坚果等人创建,团队拥有若干华为HDE,以及若干其他领域的三十余位万粉博主运营。专注于分享的技术包括HarmonyOS/OpenHarmony,ArkUI-X,元服务,服务卡片,华为自研语言,BlueOS操作系统、团队成员聚集在北京、上海、广州、深圳、南京、杭州、苏州、宁夏等地。 聚焦“鸿蒙原生应用”、“智能物联”和“AI赋能”、“人工智能”四大业务领域,依托华为开发者专家等强大的技术团队,以及涵盖需求、开发、测试、运维于一体的综合服务体系,赋能文旅、媒体、社交、家居、消费电子等行业客户,满足社区客户数字化升级转型的需求,帮助客户实现价值提升。 目前上架鸿蒙原生应用18款,三方库72个。

地址:https://atomgit.com/nutpi

https://gitcode.com/nutpi

八、FAQ

如何将Map转换为JSON字符串

可以将Map转成Record后,再通过JSON.stringify()转为JSON字符串。示例如下

  const contactInfo = new Map<string, string | string[]>();
        if (data && data.length > 0) {
          const contact: contact.Contact = data[0];

          contactInfo.set("fullName", contact.name?.fullName || "");
          const phoneNumbers: string[] = contact.phoneNumbers?.map(item => item.phoneNumber) || [];
          contactInfo.set("phoneNumbers", phoneNumbers);

          if (contact.emails && contact.emails.length > 0) {
            const emails: string[] = contact.emails.map(item => item.email);
            contactInfo.set("emails", emails);
          }
        }
        let jsonObject: Record<string, Object> = {};
        contactInfo.forEach((value, key) => {
          if (key !== undefined && value !== undefined) {
            jsonObject[key] = value;
          }
        })
        console.info(`Succeeded in selecting Contacts. data->${JSON.stringify(jsonObject)}`);

如何判断是鸿蒙平台

import 'dart:io';

 Platform.operatingSystem == 'ohos';
收藏00

登录 后评论。没有帐号? 注册 一个。