Flutter 三方库适配实战:在 OpenHarmony 上实现图片压缩功能(附超详细踩坑记录)
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
哈喽大家好呀👋!我是一名上海某高校的大一计算机新生,最近一头扎进了 Flutter for OpenHarmony 的开发大坑里(不是),一边上课一边捣鼓鸿蒙 App 开发,踩了数不清的坑,也收获了超多干货!今天就来跟大家唠唠我在项目里实现「图片压缩」功能的全过程,从依赖选型到代码实现,再到鸿蒙设备上的各种奇葩报错和解决办法,全都是我亲测有效的经验,希望能帮到同样在入门的小伙伴们!
📱 为什么我一定要做图片压缩功能?
不知道有没有和我一样的小伙伴,刚学做 App 的时候,以为图片处理就是调用一下 API,结果上手才发现:图片上传不压缩,App 直接卡成PPT,用户也会因为上传慢直接跑路!
尤其是在鸿蒙设备上,原生的图片处理 API 不仅难用,而且和 Flutter 的交互还容易出各种兼容问题。所以我就想着,能不能找一个适配鸿蒙的 Flutter 三方库,把图片压缩这件事搞定,而且还要做成一个通用的模块,后面不管是做头像上传、内容发布还是图片缓存,都能直接用。
于是就有了我项目里的「Image Compress」模块✨,它支持这几个超实用的功能:
- 质量压缩:不改变尺寸,通过降低画质减小体积
- 尺寸压缩:按比例缩小图片宽高,适配不同场景
- 批量压缩:一次性处理多张图片,不用用户一张一张传
- 格式转换:PNG/JPG 互转,满足不同的存储和上传需求
🛠️ 第一步:依赖选型,我差点踩了个大坑!
一开始我想偷懒,直接用了 pub.dev 上下载量最高的 flutter_image_compress 库,结果噩梦开始了...
❌ 踩坑 1:第三方库编译失败,鸿蒙适配不兼容
我开开心心在 pubspec.yaml 里加了依赖,运行 flutter pub get 也成功了,结果一编译鸿蒙平台的代码,直接报了一堆红,全是和原生代码相关的错误,比如找不到 .so 文件、原生方法未实现之类的。
我去翻了这个库的 issues,才发现它的鸿蒙适配还在社区适配阶段,很多原生桥接的代码都没完善,直接用根本跑不起来。对于我这种大一新生来说,自己去改原生适配代码简直是天方夜谭,只能含泪放弃这个库。
✅ 换个思路:纯 Dart 库才是鸿蒙入门的神!
后来我在 OpenHarmony TPC 的 Flutter 三方库仓库里翻了好久,发现了宝藏------纯 Dart 实现的 image 库!
它的优点简直不要太香:
- 纯 Dart 代码实现,不需要任何原生桥接,在鸿蒙上零适配就能直接用
- 功能超全,解码、编码、缩放、裁剪、格式转换全都支持
- 社区维护很活跃,文档也很全,新手也能看懂
同时我还搭配了 image_picker_ohos 这个专门适配鸿蒙的图片选择器,解决图片选择的问题,避免了官方 image_picker 在鸿蒙上的权限坑。
1. 最终的依赖配置(亲测有效!)
在 pubspec.yaml 里添加这两个依赖:
yaml
dependencies:
flutter:
sdk: flutter
image_picker_ohos: ^1.0.4 # 鸿蒙适配版图片选择器,解决相册权限问题
image: ^4.1.3 # 纯Dart图片处理库,压缩、格式转换全靠它
划重点:根据社区规范,所有代码托管平台都要使用 AtomGit,所以我直接选择了社区维护的适配版本,完全不用自己折腾,省心又省力!
2. 安装依赖,准备起飞
打开终端,在项目根目录执行命令:
bash
flutter pub get
这次终于没有报错了!依赖安装成功,接下来就是激动人心的代码环节啦~
📝 第二步:代码实现,一步一步拆解给你看
我把整个图片压缩的流程拆成了 5 个小部分,每个部分都有完整的代码和我踩过的坑,大家可以直接复制到项目里用!
1. 图片选择:从相册里选图(多图支持)
首先要让用户能从相册里选择图片,这里我用了 image_picker_ohos,而且做了多图选择的支持,用户可以一次性选好几张图一起压缩:
dart
import 'dart:typed_data';
import 'package:image_picker_ohos/image_picker_ohos.dart';
// 选择多张图片,返回图片字节流列表
Future<List<Uint8List>> pickImagesFromGallery() async {
try {
final ImagePicker picker = ImagePicker();
// 支持多图选择,设置图片质量为最高,避免选择时就被压缩
final List<XFile> selectedImages = await picker.pickMultiImage(
imageQuality: 100,
);
if (selectedImages.isEmpty) {
print("用户没有选择任何图片");
return [];
}
// 把选中的图片转成字节流,方便后续处理
List<Uint8List> imageBytesList = [];
for (var image in selectedImages) {
final bytes = await image.readAsBytes();
imageBytesList.add(bytes);
print("选中图片大小:${bytes.length / 1024} KB");
}
return imageBytesList;
} catch (e) {
print("选择图片出错:$e");
rethrow;
}
}
❌ 踩坑 2:鸿蒙相册权限没配置,一直读取失败
我第一次运行的时候,点击选择图片,App 直接没反应,也不报错,就是选不了图。查了好久才发现,鸿蒙设备上读取相册必须要在 module.json5 里配置权限!
打开 ohos/entry/src/main/module.json5,在 requestPermissions 里加上这一段:
json
"requestPermissions": [
{
"name": "ohos.permission.READ_MEDIA_IMAGES",
"reason": "$string:permission_read_media_images_reason",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
}
]
加上权限之后,重启应用,终于能正常读取相册里的图片了!鸿蒙的权限管理真的很严格,新手小伙伴们一定要记得配置,不然连图片都选不了!
2. 质量压缩:不改变尺寸,只压画质
质量压缩是最常用的压缩方式,原理就是降低图片的编码质量,在肉眼几乎看不出区别的情况下,大幅减小图片体积。
dart
import 'package:image/image.dart' as img;
// 按质量压缩图片(0-100,数值越低质量越差,体积越小)
Uint8List compressWithQuality(Uint8List originalBytes, int quality) {
// 第一步:解码图片字节流
final img.Image? originalImage = img.decodeImage(originalBytes);
if (originalImage == null) {
throw Exception("图片解码失败,可能是格式不支持");
}
// 第二步:按指定质量重新编码为JPG格式
final compressedBytes = img.encodeJpg(originalImage, quality: quality);
print("质量压缩后图片大小:${compressedBytes.length / 1024} KB");
return Uint8List.fromList(compressedBytes);
}
❌ 踩坑 3:质量设置为 0,图片直接变全黑
我一开始测试的时候,为了压到最小,直接把 quality 设置成了 0,结果压缩出来的图片直接变成了全黑的方块,差点以为代码写崩了!
后来才知道,image 库的 quality 参数范围是 0-100,0 代表最低画质,虽然体积小,但很多图片会出现严重的失真甚至无法正常显示。实际开发中,设置 70-80 是最合适的,肉眼几乎看不出区别,体积却能缩小一半以上!
3. 尺寸压缩:按比例缩小,适配不同场景
有时候用户上传的图片是几千像素的原图,直接上传不仅慢,还占服务器空间,这时候就需要按比例缩小图片的尺寸,同时保持宽高比不变,避免图片变形。
dart
// 按目标宽度等比例压缩尺寸(保持宽高比)
Uint8List compressWithSize(Uint8List originalBytes, int targetWidth) {
// 解码图片
final img.Image? originalImage = img.decodeImage(originalBytes);
if (originalImage == null) {
throw Exception("图片解码失败,可能是格式不支持");
}
// 如果原图宽度已经小于目标宽度,就不做缩放,避免放大导致模糊
if (originalImage.width <= targetWidth) {
print("图片宽度已小于目标宽度,无需缩放");
return originalBytes;
}
// 计算目标高度,保持宽高比
final double aspectRatio = originalImage.height / originalImage.width;
final int targetHeight = (targetWidth * aspectRatio).round();
print("原图尺寸:${originalImage.width}x${originalImage.height}");
print("压缩后尺寸:${targetWidth}x${targetHeight}");
// 缩放图片
final img.Image resizedImage = img.copyResize(
originalImage,
width: targetWidth,
height: targetHeight,
interpolation: img.Interpolation.linear, // 线性插值,缩放更平滑
);
// 编码为JPG返回
final compressedBytes = img.encodeJpg(resizedImage, quality: 80);
print("尺寸压缩后图片大小:${compressedBytes.length / 1024} KB");
return Uint8List.fromList(compressedBytes);
}
❌ 踩坑 4:没处理宽高比,图片直接被拉变形
我最开始写的时候,直接硬传了目标宽高,结果用户上传的竖图被压成了横图,完全变形了,巨丑无比!后来加上了宽高比计算,根据原图的宽高比自动计算目标高度,才解决了图片变形的问题,而且还加了原图尺寸判断,避免把小图放大变模糊。
4. 批量压缩:一次性处理多张图片
用户经常会一次性选好几张图片,一张一张压缩体验太差了,所以我写了一个批量压缩的方法,把上面的两个压缩方式结合起来,同时支持质量和尺寸压缩:
dart
// 批量压缩图片(先按尺寸压缩,再按质量压缩)
Future<List<Uint8List>> batchCompressImages({
required List<Uint8List> originalImages,
int quality = 80,
int targetWidth = 1080,
}) async {
List<Uint8List> compressedList = [];
for (var index = 0; index < originalImages.length; index++) {
try {
print("正在压缩第 ${index + 1} 张图片...");
// 先按尺寸压缩
final resizedBytes = compressWithSize(originalImages[index], targetWidth);
// 再按质量压缩
final compressedBytes = compressWithQuality(resizedBytes, quality);
compressedList.add(compressedBytes);
print("第 ${index + 1} 张图片压缩完成!");
} catch (e) {
print("第 ${index + 1} 张图片压缩失败:$e");
// 压缩失败时,返回原图,避免整个批量操作中断
compressedList.add(originalImages[index]);
}
}
return compressedList;
}
❌ 踩坑 5:批量压缩导致主线程卡顿,App 直接卡死
我第一次测试批量压缩的时候,一次性选了 10 张高清原图,结果 App 直接卡住了,连点击事件都没反应,差点崩溃了!
后来我才知道,图片压缩是耗时操作,如果放在主线程执行,会阻塞 UI 渲染,导致 App 卡顿甚至无响应。解决办法就是把压缩操作放到后台 isolate 里执行,用 compute 函数来实现:
dart
import 'package:flutter/foundation.dart';
// 把压缩操作放到后台执行,避免主线程卡顿
Future<Uint8List> compressInBackground(Uint8List bytes) async {
return await compute((Uint8List inputBytes) {
// 这里面的代码会在后台isolate里执行
final img.Image? image = img.decodeImage(inputBytes);
if (image == null) throw Exception("图片解码失败");
// 同时做尺寸和质量压缩
final resized = img.copyResize(image, width: 1080);
return Uint8List.fromList(img.encodeJpg(resized, quality: 80));
}, bytes);
}
改完之后,批量压缩 10 张图片,UI 依然丝滑流畅,再也不会卡顿了!
5. 格式转换:PNG/JPG 互转
很多时候用户上传的是 PNG 格式的图片,带透明通道,体积会比 JPG 大很多,所以我加了一个格式转换的功能,支持 PNG 和 JPG 互转:
dart
// 图片格式转换(支持jpg、png互转)
Uint8List convertImageFormat(Uint8List originalBytes, String targetFormat) {
final img.Image? originalImage = img.decodeImage(originalBytes);
if (originalImage == null) {
throw Exception("图片解码失败,无法转换格式");
}
switch (targetFormat.toLowerCase()) {
case "jpg":
case "jpeg":
// 转换为JPG格式,去掉透明通道
return Uint8List.fromList(img.encodeJpg(originalImage, quality: 90));
case "png":
// 转换为PNG格式,保留透明通道
return Uint8List.fromList(img.encodePng(originalImage));
default:
throw Exception("不支持的目标格式:$targetFormat");
}
}
这个功能也解决了我之前遇到的一个问题:有些 PNG 图片带透明通道,压缩成 JPG 之后会出现黑底,后来我给 encodeJpg 加了背景填充,把透明通道改成白色,就解决了黑底的问题:
dart
// 给图片填充白色背景,避免透明通道转JPG出现黑底
img.Image imageWithWhiteBackground(img.Image original) {
final img.Image newImage = img.Image(
width: original.width,
height: original.height,
);
// 填充白色背景
img.fill(newImage, color: img.ColorRgb8(255, 255, 255));
// 把原图叠加到白色背景上
img.copyInto(newImage, original);
return newImage;
}
🧪 第三步:鸿蒙设备上的完整测试
代码写好了,接下来就是激动人心的真机测试环节!我把所有功能都整合到了项目的「Image Compress」模块里,做了一个简单的 UI,用户可以选择不同的压缩方式,还能看到压缩前后的图片大小对比。
测试结果超棒!
在我的鸿蒙设备上测试,整个流程非常流畅:
- 图片选择:点击按钮,能正常打开相册,选择多张图片,没有权限报错
- 质量压缩:选择 70 的质量,一张 2MB 的图片压缩后变成了 600KB 左右,画质几乎没区别
- 尺寸压缩:把 4000x3000 的原图压缩成 1080x810,体积缩小了 80%,而且没有变形
- 批量压缩:一次性选 5 张图片,后台压缩的时候 UI 依然丝滑,压缩完成后能正常预览
- 格式转换:PNG 转 JPG 没有黑底,透明通道被正确填充成白色
测试时的最后一个坑:部分特殊格式图片解码失败
有几张用户拍的 HEIC 格式的图片,用 image 库解码的时候直接报错了,原来 image 库不支持 HEIC 格式的解码!
解决办法是:在图片选择的时候,限制只选择 JPG/PNG 格式的图片,或者给用户提示不支持的格式。我给图片选择器加了一个格式过滤:
dart
final List<XFile> selectedImages = await picker.pickMultiImage(
imageQuality: 100,
requestFullMetadata: false,
);
// 过滤掉不支持的格式
final supportedFormats = ['jpg', 'jpeg', 'png'];
List<XFile> filteredImages = selectedImages.where((file) {
final ext = file.path.split('.').last.toLowerCase();
return supportedFormats.contains(ext);
}).toList();
这样就不会再遇到解码失败的问题了,用户也不会因为选择了不支持的格式而导致功能报错。
💡 大一新生的踩坑心得总结
这次实现图片压缩功能,我前前后后折腾了快一个星期,踩了无数的坑,也学到了超多东西,给大家总结几个新手必看的要点:
-
依赖选型优先选纯 Dart 库 :对于鸿蒙入门来说,纯 Dart 实现的三方库比依赖原生的库兼容性好太多了,像
image这种完全不用适配,拿来就能用,新手友好度拉满! -
鸿蒙权限一定要提前配置 :不管是相册、相机还是存储,只要用到系统能力,一定要在
module.json5里配置权限,不然 App 只会静默失败,连个报错都没有! -
耗时操作一定要放到后台 :图片压缩、网络请求这些耗时操作,一定要用
compute或者Isolate放到后台执行,不然主线程阻塞,App 直接卡顿甚至崩溃! -
一定要做异常捕获和兼容处理:比如图片解码失败、格式不支持、压缩失败这些情况,都要加 try-catch 处理,给用户友好的提示,而不是直接让 App 崩溃。
-
多去社区仓库找适配好的库:OpenHarmony TPC 的 Flutter 三方库仓库里,有很多社区维护的适配好的库,比自己去 pub.dev 瞎找靠谱多了!
-


