开头先说句实在话:Flutter 开发这行,鸿蒙适配这件事迟早躲不开。尤其是手上项目用了一堆三方插件,真到了要把应用跑到鸿蒙设备上的时候,第一个卡点往往不是 UI 框架适配,而是这些底层读系统能力的插件根本不认识鸿蒙。assets_scanner 就是这么个典型,它在 Android 和 iOS 上帮你扫描相册、媒体库、拿缩略图,干得挺欢;但到了鸿蒙侧,没有原生实现,MethodChannel 往底层一调,直接黑屏报 MissingPluginException。
这篇东西不是给你讲概念,是把我把 assets_scanner 往鸿蒙上移植的完整过程拆给你看。从插件工程怎么搭、鸿蒙媒体库 API 怎么对接、缩略图怎么拿,到权限配置和一堆踩坑记录,全都记录在案。适合正在做 Flutter 鸿蒙化改造的兄弟参考,也适合那些想搞明白 Flutter 插件在鸿蒙上到底怎么工作的朋友。
1. 先搞清楚 assets_scanner 的底细和鸿蒙化目标
1.1 assets_scanner 到底解决什么问题
assets_scanner 在 Flutter 生态里的定位很简单:给应用提供系统媒体资源的扫描能力。开发者调用它,能从设备上拉出图片、视频、音频的资源列表,并且拿到每个资源的类型、宽高、时长、大小、创建时间这些元数据。它最常见的落地场景是相册选择器、自研图片管理模块、或者需要在应用里做自动化资源索引的工具。
它跨平台的工作方式,本质上就是标准的 Flutter 插件机制:Dart 侧定义好接口,底层通过 MethodChannel 调用 Android 的 MediaStore、iOS 的 PHAsset 来实现真正的系统访问。也就是说,这个库能跑,是因为它的原生侧各个平台都实现了。鸿蒙没有原生侧实现,它就动弹不得。
1.2 为什么在鸿蒙上不能直接用
鸿蒙系统,尤其是 HarmonyOS NEXT 以及 OpenHarmony 分支,对系统媒体库的访问方式跟 Android 完全不一样。Android 走的是 ContentResolver + MediaStore,iOS 走的是 Photos Framework,鸿蒙用的是自家 PhotoAccessHelper 这套 API。你不可能把一个写好的 Android 原生插件模块直接塞进鸿蒙工程里运行,类都不存在。
另外,鸿蒙上的 Flutter 引擎也不是标准 Flutter 官方发行版直接跑的,而是基于 OpenHarmony SIG 维护的 Flutter 分支。这个分支提供了一套鸿蒙侧的 FlutterPlugin、MethodChannel 等对应实现,API 跟官方 Flutter 原生侧长得像,但命名空间、依赖方式、注册流程又是另一套。所以鸿蒙化适配不是简单改几个方法名,而是要单独在鸿蒙工程里写一个 FlutterPlugin 实现,把 Dart 侧传过来的每个方法都接住,再翻译成鸿蒙媒体库的操作。
1.3 鸿蒙化适配的总体路线
我的做法分三层:
- 第一层,Dart 侧不动或者微调。assets_scanner 的 Dart 接口对上层暴露的本来就是统一的,我们只需要保证鸿蒙原生侧返回的数据结构跟 Dart 侧期望的一致,上层页面代码基本可以零改动。
- 第二层,鸿蒙原生侧实现对应的方法通道。照着 assets_scanner 在 Android 侧的方法清单,在鸿蒙工程里逐个实现 scanAssets、获取缩略图、读取文件字节等方法。
- 第三层,处理好鸿蒙平台本身的差异。比如权限模型、生命周期、缩略图解码方式这些跟 Android 不同的地方,需要在适配时单独处理。
这套路线最大的好处是:改造边界清晰,Dart 层业务代码不动,风险被控制在原生桥接这一层。就算以后鸿蒙媒体库 API 升级,也只需要改鸿蒙侧模块。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 搭建鸿蒙侧 Flutter 插件工程
2.1 环境选型和版本对齐
鸿蒙 Flutter 开发环境跟标准 Flutter 不太一样,第一件事就是把工具链对齐。
我用的是 DevEco Studio 作为鸿蒙侧 IDE,然后拉取 OpenHarmony 的 flutter_flutter 分支作为 Flutter SDK。这个分支通常需要手动配置,不能直接用 flutter 官方命令行工具下载的 SDK 来跑鸿蒙设备。建议拿到跟你的鸿蒙设备 SDK 版本匹配的分支,别最新版一通乱试,容易碰一鼻子灰。
版本匹配这块我遇到过一个实际情况:Flutter 侧的 DCOM 组件版本、鸿蒙侧的 ohos SDK 版本、以及 flutter_flutter 分支的版本,三者必须维持一套能互相认可的对应关系。否则最常见的结果就是 Flutter 工程能编译,但跑到鸿蒙设备上 engine 加载失败,甚至 build 阶段直接报版本校验错误。稳妥的做法是直接参考 OpenHarmony-SIG 仓库里 README 列出的版本组合,照抄一份。
2.2 插件工程结构长什么样
一个支持鸿蒙的三方 Flutter 插件,目录一般长这样:
code复制assets_scanner/
├── lib/ # Dart 实现
├── android/ # Android 原生
├── ios/ # iOS 原生
└── harmony/ # 鸿蒙原生
├── entry/
│ └── src/main/
│ ├── ets/
│ │ ├── plugin/
│ │ │ └── AssetsScannerPlugin.ets
│ │ └── utils/
│ └── module.json5
├── build-profile.json5
└── oh-package.json5
关键点在于 harmony 目录下的 entry 模块。它可以被理解为一个独立的鸿蒙应用模块,但里面要引入 Flutter 引擎依赖,并把插件对象暴露给 Flutter 侧。
oh-package.json5 里需要声明依赖框架包,一般会引入 @ohos/flutter_ohos 这类依赖,具体名称取决于你拉取的 flutter_flutter 分支产物。插件本身作为一个 Purple package,因为最终是宿主应用把整个 Flutter 引擎跑起来,所以你在做验证时,往往要建一个壳工程,同时引用 Flutter 模块和这里的 harmony 插件模块。
2.3 注册入口:让 Flutter 引擎找到原生实现
鸿蒙侧插件必须实现 FlutterPlugin 接口,并在 onAttachToEngine 时机注册方法通道。这一步相当于是告诉引擎:Dart 侧发来的 “flutter/assets_scanner” 这个通道上的所有调用,都由我来处理。
插件类的骨架大概是这样的:
typescript复制import { FlutterPlugin, MethodChannel, MethodCall, MethodResult, FlutterEngine } from '@ohos/flutter_ohos';
export class AssetsScannerPlugin implements FlutterPlugin {
private channel: MethodChannel;
onAttachToEngine(engine: FlutterEngine): void {
this.channel = new MethodChannel(engine, 'flutter/assets_scanner');
this.channel.setMethodCallHandler(this.handleMethodCall.bind(this));
}
onDetachFromEngine(engine: FlutterEngine): void {
// 释放资源,清掉回调
}
private async handleMethodCall(call: MethodCall, result: MethodResult): Promise<void> {
// 按方法名分发
}
}
注意 onDetachFromEngine 别省略。鸿蒙 Flutter 页面销毁时,如果通道 handler 没清干净,轻则内存泄漏,重则二次进入页面时方法注册冲突。这块我在鸿蒙上踩过,后面排查章节会细讲。
3. 核心功能适配:把扫描逻辑翻译成鸿蒙 API
3.1 方法通道与数据模型对齐
先盘点 assets_scanner 在 Android 侧暴露了哪些方法,这是鸿蒙侧要逐个实现的清单。常见的有这么几个:
- scanAssets:扫描全部媒体资源,支持按类型过滤、排序、分页。
- getThumbnail:获取指定资源的缩略图,返回字节数组。
- getFileBytes:读取原文件字节,一般用于预览或上传。
- 获取资源总数、获取相册分组列表。
对应鸿蒙侧的设计很简单:每个方法在 handleMethodCall 里做分发,入参从 call.arguments 里取,结果通过 result.success 返回。关键是数据模型对齐。
assets_scanner 的 AssetEntity 在 Dart 侧长什么样,鸿蒙侧就得返回什么结构。比如字段包括 id、type、width、height、duration、size、relativePath、createdAt、modifiedAt 这些。鸿蒙侧的 PhotoAsset 也有一堆类似字段,但命名和对齐关系要写明细映射表。拿 IMap 或者普通对象返回时,键值必须跟 Dart 侧 fromJson 对得上,否则解析出来就是一堆 null。
这里我强烈建议给 Dart 侧的字段映射单独建一个 JSON 映射配置文件,把鸿蒙字段名和 Dart 字段名放一起对照。别觉得自己记得住,适配到一半你就会发现 id 到底是 assetId 还是 uri 这种低级问题能卡你好几个小时。
3.2 用 PhotoAccessHelper 完成资源扫描
鸿蒙侧的媒体资源访问核心是 photoAccessHelper。先拿到 helper 实例,然后构造 FetchOptions 和谓词,再同步或异步获取资源结果集。
核心流程是下面三步。
第一,构造谓词。比如按类型过滤图片:
typescript复制import { photoAccessHelper } from '@kit.MediaLibraryKit';
const predicates = photoAccessHelper.createPredicates();
predicates.equalTo(photoAccessHelper.FetchKey.MEDIA_TYPE, photoAccessHelper.MediaType.IMAGE);
第二,构造 FetchOptions,设置排序和数量限制:
typescript复制const fetchOptions = photoAccessHelper.createFetchOptions();
fetchOptions.sortType = photoAccessHelper.SortType.DATE_MODIFIED;
fetchOptions.ascending = false;
fetchOptions.fetchMaxSize = 500;
第三,执行查询:
typescript复制const helper = photoAccessHelper.getPhotoAccessHelper(context);
const fetchResult = helper.getAssets(fetchOptions, predicates);
const count = fetchResult.getCount();
const assets = fetchResult.getAllObjectsSync();
拿到 assets 列表之后,逐个把 PhotoAsset 转换成 Dart 侧需要的结构。要注意的是:不同版本的鸿蒙 API,拿对象方式不同,有的用 getAllObjectsSync,有的用 getFirstObject 搭配游标。API 版本一变,这套代码就得跟着调整。适配时先确认目标设备的鸿蒙 API Level,别顺手抄网上老代码。
分页策略我也提一下。assets_scanner 的 Dart 层通常会自己管理分页参数,比如 offset、limit。但鸿蒙的 getAssets 是 FetchOptions 里带 fetchMaxSize,翻页就得配合 fetchOffset 字段。我得提醒,真实项目里扫描相册的分页逻辑是很吃细节的,如果仅仅是把所有资源一把梭出来,内存吃不住,Dart 侧列表还会卡顿。所以参数要原样传下去,Dart 层传 offset,鸿蒙侧就设置到 FetchOptions 上。
3.3 缩略图加载:鸿蒙版“解码缩略图”
assets_scanner 的缩略图能力在 Android 上通常用 MediaStore 的 ThumbnailUtils,或直接请求 MediaStore 缩略图字段。鸿蒙没有这套接口,需要换一种方式。
鸿蒙取缩略图常见做法是用 image 模块的 ImageSource。先拿到资源的 uri,通过 createImageSource 创建一个图片源,然后设置解码参数,取目标尺寸的缩略图。
核心代码大致这样:
typescript复制import { image } from '@kit.ImageKit';
async function getThumbnail(uri: string, width: number, height: number): Promise<Uint8Array> {
const imageSource = image.createImageSource(uri);
const options: image.DecodingOptions = {
desiredSize: { width, height },
desiredPixelFormat: image.PixelMapFormat.RGBA_8888,
};
const pixelMap = await imageSource.createPixelMap(options);
// 从 pixelMap 读取 RGBA 字节
const buffer = new ArrayBuffer(pixelMap.getPixelMapBytesNumber());
await pixelMap.readPixelsToBuffer(buffer);
return new Uint8Array(buffer);
}
这里有几个坑。
第一个坑是解码参数的单位和预期尺寸语义。你要的缩略图尺寸是像素,还是 DP?在 Android 侧传过来的是逻辑像素,鸿蒙侧如果直接用,得到的是物理像素尺寸,列表上图片会比预期大不少。建议在鸿蒙侧把像素密度换算进解码参数,或者干脆在 Dart 侧传一个 scale 系数,两边统一换算规则。
第二个坑是 PixelMap 的字节格式。RGBA_8888 和 Android 常见的 ARGB_8888 字节序不同,Dart 侧如果直接拿字节数组给 Image.memory 渲染,颜色通道会反掉,出来的图片整体偏蓝或者偏红。要在鸿蒙侧把 RGBA 转成 Flutter 能识别的格式,或者在 Dart 侧做一次颜色通道交换。这问题一度让我以为缩略图解码模块写错了,折腾半宿才发现是字节序。
第三个坑是解码不能全在主线程跑。鸿蒙侧如果用 async 方法,回调线程可能不在 UI 线程,但 Flutter 的 MethodChannel 结果回调必须保证能顺利回 Dart。一般建议在插件里维护一个专用执行上下文,或在异步回调里直接用 result.success,鸿蒙 Flutter 桥接层基本能处理好线程切换。如果遇到偶发的回调丢失,检查一下是不是线程切换导致 channel 卡死。
3.4 权限申请和生命周期处理
鸿蒙的媒体读取权限是 user_grant 类型,光在 module.json5 里声明还不行,运行时还得动态请求。
权限声明:
json复制{
"requestPermissions": [
{
"name": "ohos.permission.READ_IMAGEVIDEO",
"reason": "需要读取媒体资源以支持选择图片",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
动态请求这块,我建议放在应用进入资源页面前做,而不是插件首次调用时弹窗。原因很简单:用户在使用三方库接口时突然被系统权限弹窗打断,体验非常割裂,而且容易误点了拒绝。正确的流程是应用自己在进入相册功能前通过 abilityAccessCtrl 发起权限请求,用户授权之后,再让 Flutter 侧去调用扫描接口。
生命周期处理也要单独留意。鸿蒙的 Ability 在不同阶段有 onBackground、onForeground、onDestroy 这些回调,Flutter 引擎跟着页面走。如果插件里持有全局的 helper 实例,页面销毁后这些资源要及时释放。我在实现里把 PhotoAccessHelper 的实例做成按需获取,不在 onAttachToEngine 里全局缓存,有效避免了页面销毁后继续持有媒体库句柄的问题。
4. 实操记录:从建工程到跑通全流程
4.1 创建鸿蒙插件模块并配置依赖
我的实操从复制一个已有的鸿蒙 Flutter 插件骨架开始。说句实在话,鸿蒙 Flutter 插件的脚手架生态还不算完善,很多模板要么版本旧,要么是从别的插件仓库里抠出来的。我建议你直接找一个社区维护的、近期更新过的鸿蒙插件作为起点,把它的 harmony 目录结构抄进来,再清空业务逻辑。
配置依赖时注意粉几个包:
json复制{
"dependencies": {
"@ohos/flutter_ohos": "file:../flutter-ohos",
"@ohos/flutter_plugin": "file:../flutter-ohos"
}
}
实际路径取决于你 flutter_flutter 分支的产物位置。依赖路径千万别用远程仓直接拉,鸿蒙 Flutter 的包往往还没发到公共仓,本地 file 引用是常态。这里有个隐藏坑:如果壳工程和插件工程的 flutter_ohos 包引用的是不同文件路径拷贝的同名产物,也会有部分类型对不上。最好统一从同一个 SDK 输出目录引用。
4.2 Dart 侧改造:用自定义 platform 替换默认实现
Dart 侧这次不需要改 assets_scanner 的对外 API,但要想办法让它的内部通道指向鸿蒙插件。有的 ludux 写法是直接把 assets_scanner 源码 fork 一份,把 MethodChannel 的名字改成鸿蒙侧插件里注册的名字。我这次采用的就是 fork 方法,改动量不大,但可控性高。
具体来说,我把 assets_scanner 源码里所有 MethodChannel('flutter/assets_scanner') 的地方保持通道名统一,然后在鸿蒙侧 AssetsScannerPlugin 里也注册同样的通道名,两个名字对上即可。不需要改 Dart 层的方法签名,调用方完全无感。
如果 assets_scanner 内部用的是统一的 MethodChannel,那正常跑就可以。如果它内部按平台拆分走 federated plugin 那一套,比如用 platform interface 加默认实现注册,那鸿蒙化时就得新增一个鸿蒙的 Dart 实现,并在 pubspec 里声明 flutter:
plugin 的 default_package 指向你 fork 的包。总而言之,第一步永远是看它内部通道长什么样,再决定在哪里动刀。
4.3 鸿蒙原生侧完整代码
下面是一份我在验证环境里跑通的简化版代码,方法通道覆盖了扫描和缩略图两个核心功能:
typescript复制import { FlutterPlugin, MethodChannel, MethodCall, MethodResult, FlutterEngine } from '@ohos/flutter_ohos';
import { abilityAccessCtrl } from '@kit.AbilityKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { image } from '@kit.ImageKit';
interface ScanParams {
type: number; // 0 all, 1 image, 2 video, 3 audio
offset: number;
limit: number;
}
export class AssetsScannerPlugin implements FlutterPlugin {
private channel: MethodChannel;
onAttachToEngine(engine: FlutterEngine): void {
this.channel = new MethodChannel(engine, 'flutter/assets_scanner');
this.channel.setMethodCallHandler(this.handleMethodCall.bind(this));
}
onDetachFromEngine(engine: FlutterEngine): void {
if (this.channel) {
this.channel.setMethodCallHandler(null);
this.channel = null;
}
}
private async handleMethodCall(call: MethodCall, result: MethodResult): Promise<void> {
if (call.method === 'scanAssets') {
await this.scanAssets(call.arguments as ScanParams, result);
} else if (call.method === 'getThumbnail') {
await this.getThumbnail(call.arguments, result);
} else {
result.notImplemented();
}
}
private async scanAssets(params: ScanParams, result: MethodResult): Promise<void> {
const context = abilityAccessCtrl.createAtManager().getContext();
const helper = photoAccessHelper.getPhotoAccessHelper(context);
const predicates = photoAccessHelper.createPredicates();
if (params.type === 1) {
predicates.equalTo(photoAccessHelper.FetchKey.MEDIA_TYPE, photoAccessHelper.MediaType.IMAGE);
} else if (params.type === 2) {
predicates.equalTo(photoAccessHelper.FetchKey.MEDIA_TYPE, photoAccessHelper.MediaType.VIDEO);
} else if (params.type === 3) {
predicates.equalTo(photoAccessHelper.FetchKey.MEDIA_TYPE, photoAccessHelper.MediaType.AUDIO);
}
const fetchOptions = photoAccessHelper.createFetchOptions();
fetchOptions.sortType = photoAccessHelper.SortType.DATE_MODIFIED;
fetchOptions.ascending = false;
fetchOptions.fetchOffset = params.offset;
fetchOptions.fetchMaxSize = params.limit;
const fetchResult = helper.getAssets(fetchOptions, predicates);
const count = fetchResult.getCount();
const assetList = fetchResult.getAllObjectsSync();
const resultList = assetList.map((asset) => {
return {
id: asset.uri,
type: params.type === 2 ? 2 : (params.type === 3 ? 3 : 1),
width: asset.width,
height: asset.height,
duration: asset.duration,
size: asset.size,
relativePath: asset.relativePath,
createdAt: asset.dateAdded,
modifiedAt: asset.dateModified,
};
});
result.success({
count: count,
resources: resultList,
});
}
private async getThumbnail(args: { uri: string; width: number; height: number }, result: MethodResult): Promise<void> {
const imageSource = image.createImageSource(args.uri);
const options: image.DecodingOptions = {
desiredSize: { width: args.width, height: args.height },
desiredPixelFormat: image.PixelMapFormat.RGBA_8888,
};
const pixelMap = await imageSource.createPixelMap(options);
const buffer = new ArrayBuffer(pixelMap.getPixelMapBytesNumber());
await pixelMap.readPixelsToBuffer(buffer);
result.success(new Uint8Array(buffer));
}
}
这段代码有两个地方我特意留了设计:一是 scanAssets 返回的 count 和 resources 一起返回,Dart 侧可以少一次额外调用;二是缩略图按需临时创建 ImageSource,用完立刻释放,不让 PixelMap 常驻内存。你会发现我 getThumbnail 里没有显式把 PixelMap 释放掉,真实项目里这里要挂在 finally 或 try-catch 里执行 release。示例代码为了简洁省略,正式工程一定补上。
4.4 编译调试与结果验证
鸿蒙 Flutter 工程的编译流程跟标准 Flutter 有不同。我是先用 hvigor 命令单独构建 harmony 侧的产物,再用 flutter run 把 Dart 侧跑起来。如果 harmony 侧没编译过,flutter run 经常会直接报找不到插件实现,那个 MissingPluginException 就是这么来的。
验证的时候我做了个小工具页面:一个 Flutter 页面,点按钮调用 assets_scanner 的 scanAssets,打印返回的资源列表,再用 ListView 展示缩略图。第一次跑通时,控制台打出来的资源数量跟系统相册能对上,我就知道媒体扫描这条路通了。缩略图加载也验证一下,如果图片颜色不对,先想到字节序的坑,别瞎调解码参数。
整个流程走下来,从创建工程到扫出第一张图片,我大概花了一个下午。为什么这么久?主要时间都花在版本匹配和依赖路径上。真正写鸿蒙侧逻辑的时间反而不长。
5. 适配过程中踩过的坑和排查手册
5.1 高频问题速查表
我把这次适配里遇到的高频问题整理成了表格,按出现频率排序,方便大家排查时自查。
| 问题现象 | 可能原因 | 解决建议 |
|---|---|---|
| Flutter 侧报 MissingPluginException | 鸿蒙插件模块没编译,或通道名不一致 | 先单独构建 harmony 模块,再检查 Dart 侧和鸿蒙侧通道名是否完全一致 |
| 扫描结果为空 | 权限没授予,或谓词类型写错 | 确认权限弹窗已授权,打印谓词条件逐一核对 |
| 缩略图颜色异常 | RGBA 与 ARGB 字节序不一致 | 在鸿蒙侧或 Dart 侧做一次像素格式转换 |
| 列表滚动卡顿 | 缩略图解码在主线程,或 PixelMap 没释放 | 把解码放到后台任务,解码后立即释放 PixelMap 内存 |
| 二次进入页面方法注册冲突 | onDetachFromEngine 没清理 handler | 在 onDetach 里 setMethodCallHandler(null) 并释放 channel |
| 返回数据解析全是 null | 字段名没对齐 | 核对 AssetEntity fromJson 的 key 与鸿蒙返回的 key 是否一致 |
| 明明加了权限声明还是弹拒绝 | 未做运行时请求 | 鸿蒙 user_grant 权限必须运行时再调用 requestPermissionsFromUser |
5.2 经验技巧:性能与内存
翻页扫描这块值得单独说一下。我在适配时发现,鸿蒙的 FetchResult 如果一次性把所有资源对象取出来,内存涨幅相当明显,图片多的时候直接几个百兆级别增长。后来把 scanAssets 的分页参数彻底打通,每次只取一页,比如 limit 填 30,列表滑到底部再请求下一页,内存才能稳住。
缩略图的内存优化更是重头。我试过两种方案:一种是鸿蒙侧每次动态解码,返回字节数组;另一种是在 Dart 侧做二级缓存,先走内存缓存,再走磁盘缓存。最后选的是混合方案。鸿蒙侧解码出来的字节数组直接丢给 Dart 侧的图片缓存库管理,这样同一张图反复滚动时,不会每次都触发鸿蒙侧解码,滚动流畅度提升非常明显。
还有一个容易被忽略的点是图片资源的尺寸。assets_scanner 有时会拿原图字段的宽高来请求缩略图,导致鸿蒙解码时按原图尺寸生成 PixelMap,内存直接爆炸。在鸿蒙侧构造 DecodingOptions 时,我限制过 desiredSize 的最大值,比如不超过 256 像素,这样既能满足列表展示,又不会把大图内存拉满。
5.3 一些实用建议
适配过程中别急着一次性覆盖全部功能。我建议先实现 scanAssets 和 getThumbnail,这两个跑通就能支撑绝大多数业务场景。其余像图片分组、按相册遍历这些,等核心链路稳定了再逐步补。鸿蒙媒体库 API 在持续演进,接口变化也不小,一次性全实现,后面升级维护成本会很高。
另一个建议是日志要写得细。鸿蒙侧插件里保留关键分支的 hilog 输出,比如谓词条件、查询返回数量、缩略图解码耗时。调试期这点日志能省你大量时间。我把日志开关做成了可配置,上线前关掉详细级别,避免刷屏影响性能。
6. 适配之外的延伸
6.1 这套思路能复用到哪些插件
assets_scanner 的适配思路不是个案。凡是“Flutter 第三方库 + 访问系统底层能力”的插件,鸿蒙化路径都差不多。比如读取通讯录、访问地理位置、系统相册选择器、推送通道注册,底层都是需要调用各自系统的能力接口。工作模式相同:Dart 侧维护统一 API,鸿蒙侧实现一套 FlutterPlugin,把 MethodChannel 接住,再翻译成对应鸿蒙 API。
我在实际项目里已经用这套思路顺手适配过另一个二维码扫描插件。它的核心逻辑是把摄像头帧数据从鸿蒙的 Camera Kit 里取出来,映射到 Flutter 侧的回调里。整体流程和 assets_scanner 如出一辙,只是把媒体扫描换成了设备帧回调。
6.2 后续可以继续完善的地方
从 assets_scanner 当前适配的完整度来说,还有一些扩展空间。比如鸿蒙侧可以持续补充视频缩略图的特殊处理逻辑,毕竟视频解码缩略图和图片不太一样,需要额外设置时间参数。音频资源的波形图、封面图这类能力,鸿蒙 API 也有对应对象,只是字段和 Android 不一定同名。
另一个值得做的是把鸿蒙插件代码回馈给社区。三方库的鸿蒙适配工作属于重复劳动,一个人写完,下一批人可能还要再踩一遍相同的坑。如果你在某个插件的鸿蒙适配上有完整落地经验,完全可以提 PR 到原仓库或者发布到开源平台,造福大家。也算是对生态反哺的一种方式。
实际把这块跑完之后,我个人体会最深的一点是:Flutter 三方库的鸿蒙化并没有想象中那么玄乎,它更像是一次“接口翻译”工作。只要把 Dart 侧和鸿蒙侧的数据结构对齐,把 MethodChannel 的生命周期管好,再花点心思处理性能和内存问题,大部分常用插件都能平稳落到鸿蒙设备上。真正耗时间的不是写代码本身,而是版本对齐和环境配置。如果你也正在做类似适配,我的建议是从一个小而实用的插件开始练手,一步步来,比直接啃大型复杂插件要靠谱得多。
