最近整理了一个 Flutter 跨平台开发鸿蒙应用的完整项目,业务方向是“附近自助照相馆”。这类应用听起来不复杂,但真正动手做,你会发现从环境搭建到多端适配,每一个环节都藏着不少坑。这篇教程就围绕这个项目,把需求拆解、环境准备、功能实现、问题排查到最后的优化上线,一条线讲清楚。
做这个项目的初衷很简单:自助照相馆本身处于一个快速扩张的阶段,商场、地铁站、写字楼里越来越多,但用户找店非常依赖美团这类平台,无法感知附近哪家店有空位、有什么套餐、怎么过去。如果有一个轻量应用,能直接定位出最近的店、看到实时空闲状态、扫码开柜、支付然后取片,就是一个很完整的闭环。而且这种工具类应用最适合用 Flutter 来做一套代码多端复用——Android、iOS、鸿蒙全兼容。
这套东西很适合两类人去参考:一是准备在鸿蒙上落地 Flutter 业务的团队,二是想用跨平台方案做一个带地图、支付、扫码等综合功能的实战项目来练手的开发者。接下来我按项目推进的顺序,把每个阶段的核心思路、技术选型、实操代码和踩坑记录都写出来。
1. 项目背景与需求拆解
1.1 自助照相馆的业务形态与用户痛点
自助照相馆和传统影楼最大的区别是无人化、标准化、流程短。用户进到一个几平米的包厢里,在触控屏上选套餐、拍照、修图、付款,照片直接打印出来,全程不需要店员干预。这种方式能把单店成本压得非常低,所以这几年扩张速度很快。
但站在用户体验角度,痛点也很明显:
- 用户根本不知道最近的照相馆在哪里,即使知道,也无法判断那家店有没有人占着、设备是否正常。
- 价格不透明,不同门店、不同时段的套餐可能不同,缺乏直观展示。
- 到店后可能还要扫码开柜存放物品、扫码启动设备,这个流程如果没有App配合,全靠现场设备交互,体验会很零散。
- 照片一般会上传到云端,用户如果想在手机上随时查看、下载或打印历史照片,没有一个统一入口。
做这个应用的目标就是解决这些问题,把“发现门店—查看详情—到店开柜—拍照支付—获取照片”串成一个平滑的闭环。而且这种应用不只适用于单一品牌,甚至可以聚合周边的多家自助照相馆,做成一个平台型工具。
1.2 功能模块与跨平台技术选型
我把核心功能拆成了几个模块:
- 地图与门店列表:基于定位获取附近的门店,支持地图展示、距离排序、门店详情页。
- 设备状态与扫码开柜:显示每个包厢/柜子的空闲状态,用户到店后扫码,后端返回开锁指令。
- 拍照与照片管理:支持直接调用相机拍摄,或者从相册上传,简单裁剪、滤镜、加水印,最后上传到云端。
- 订单与支付:套餐选择、生成订单、拉起微信/支付宝/华为IAP支付,支付回调后更新订单状态。
- 用户中心:注册登录、个人照片集、历史订单、优惠券。
技术选型上,最初纠结过用 ArkTS 写鸿蒙原生,再用 Kotlin/Swift 分别写 Android 和 iOS,但算了一下工作量,三个端三套团队,排期至少要翻倍。后来确认 Flutter 已经能跑到鸿蒙(OpenHarmony)平台,而且性能和 UI 一致性都有不错的表现,这才最终定了 Flutter。
Flutter 在这里的价值是:UI 层完全复用,地图、扫码、支付这类依赖系统能力的功能通过插件层做适配。也就是说,业务层和页面层写一套,只有第一层系统交互才需要为鸿蒙写平台通道。对于附近照相馆这种以列表、地图、表单、支付为主的应用,Flutter 的覆盖度非常高。
2. 环境准备:让 Flutter 真正跑上鸿蒙
2.1 Flutter 支持鸿蒙的原理与坑
先说清楚底层原理,这样后面遇到问题才好排查。
鸿蒙系统从 HarmonyOS NEXT 开始不再兼容 Android APK,所以 Flutter 官方版本不能直接在真机上跑,必须使用 OpenAtom 基金会维护的 Flutter 分支,它基于官方 Flutter 做了一套 ohos 平台的适配,把 Flutter 的 engine 对接到鸿蒙的 ArkUI 框架和系统能力上。
这套适配的核心机制是:Flutter 项目里增加了一个 ohos 平台目录,里面是用 ArkTS 写的 Runner 工程,相当于一个鸿蒙原生的壳,Flutter 的 Dart 代码渲染到 ArkUI 的画布上。因此 Flutter 官方插件需要注意是否支持 ohos,不支持的就得自己写 MethodChannel 调原生 API。
对于“附近自助照相馆”这类应用,我建议从一开始就确认好要用的插件有没有 ohos 适配版本,比如地图、支付、扫码、相册等。如果暂时没有,就要预留出自己实现平台通道的时间。这块是跨平台开发里最容易低估的风险。
2.2 详细的搭建步骤
我当前的开发环境是 Windows 11 + DevEco Studio + 原生 Flutter SDK 的 ohos 适配版。具体步骤可以这样操作:
- 安装 Flutter SDK,建议选择 3.16 或更高版本(ohos 适配分支有对应版本),然后把 bin 目录加入系统 PATH。
- 安装 DevEco Studio 和 HarmonyOS SDK。DevEco 是鸿蒙的 IDE,会默认下载好 SDK,把它安装到默认路径。
- 下载 Flutter 的 ohos 适配分支代码。这里用社区维护的版本,比如 gitee 上 mirror 的 flutter_flutter 仓库,切到对应版本分支,或者直接下载 release 包解压,配置
FLUTTER_STORAGE_BASE_URL和PUB_HOSTED_URL为国内镜像。 - 在命令行执行
flutter doctor,确认flutter、dart、DevEco Studio都被识别。 - 创建项目时加上 ohos 平台:
flutter create --org com.example.photobooth --platforms=android,ios,ohos .如果创建时没有 ohos 选项,说明 Flutter 版本不对,检查一下版本与鸿蒙适配要求的匹配关系。
用 DevEco Studio 打开项目根目录下的
ohos文件夹,第一次会自动同步 Gradle 和鸿蒙依赖。这个同步过程很考验网络,尽量配置好镜像源再开始。真机连接并开启开发者模式,命令行运行:
flutter run -d ohos如果能看到默认的计数器页面跑起来,说明环境通了。
注意:HarmonyOS NEXT 真机上调试需要在 DevEco 里配置签名,否则应用无法安装。建议先去华为开发者网站申请调试证书,把自动签名配置好。
3. 核心功能实现:附近门店、扫码开柜与支付
3.1 地图与定位模块的实现
地图我直接选用了高德地图的 Flutter 插件,因为它同时支持 Android 和 iOS,而出于鸿蒙适配考虑,我在 ohos 目录里通过 MethodChannel 调用了华为地图的 SDK。这样做虽然要写两个平台通道,但业务层只暴露了一个地图容器,页面代码完全统一。
先看 Dart 侧的地图加载:
class NearByStoreMap extends StatelessWidget { @override Widget build(BuildContext context) { return _MapContainer( onMapCreated: (MapController controller) { // 设置中心点为当前定位位置 controller.setCenter(LocationManager.instance.latLng); controller.showNearbyStores(StoreRepository.instance.stores); }, ); } } class _MapContainer extends StatelessWidget { @override Widget build(BuildContext context) { if (Platform.isAndroid || Platform.isIOS) { return AMapWidget( onMapCreated: ..., ); } else if (Platform.isOHOS) { return HuaweiMapWidget( onMapCreated: ..., ); } } }定位权限方面,Android 和鸿蒙的配置不一样:
- Android 需要在
android/app/src/main/AndroidManifest.xml里声明ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION。 - 鸿蒙需要在
ohos/entry/src/main/module.json5里的requestPermissions数组里加上:
{ "name": "ohos.permission.LOCATION", "reason": "用于获取附近门店位置", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }实际开发中,很多权限问题都出在这类原生配置遗漏上。定位信息获取后,需要计算与每家门店的距离,我直接用高德和华为 SDK 自带的 distance 方法,然后再按距离排序。前端排序速度很快,门店数量不超过几百家时完全不用上后端排序。
3.2 扫码开柜与设备状态管理
自助照相馆的开柜逻辑是:用户到店后,包厢门口的屏幕上会显示一个二维码,里面包含门店 ID、包厢号和一次性的开柜令牌。用户在小程序或 App 里扫码,App 解析二维码后调用后端开柜接口,后端校验令牌并下发开锁指令。
在 Flutter 里,扫码我用了mobile_scanner插件,这个插件的 ohos 适配是社区补的,版本兼容性还可以。如果后续遇到相机打开失败的问题,基本就是相机权限没配置好。
扫码页面的核心逻辑简化后是这样:
Future<void> handleBarcode(String rawValue) async { final codeInfo = QrCodeParser.decode(rawValue); if (codeInfo == null) { setState(() => _error = '无效的二维码'); return; } final result = await api.openLocker( storeId: codeInfo.storeId, boxNo: codeInfo.boxNo, token: codeInfo.token, ); if (result.success) { // 跳转到拍照引导页面 Navigator.pushNamed(context, '/photograph', arguments: result.orderId); } }这里有几个细节值得注意:
- 开柜令牌是一次性的,解析后必须立即校验,过期或重复使用后端都要拒绝。
- 开柜动作是异步的,后端可能要等设备响应,前端不能一直转圈。我当时的处理是:请求接口后同时建立 WebSocket 监听,等待设备状态变更消息,如果 10 秒内没有响应就提示用户联系客服,并提供手动输入密码的开柜备用方案。
- 设备状态轮询不要用自绘的 Timer 硬刷,最好用服务端推送,或者至少用 StreamBuilder 配合周期性的刷新,避免页面频繁重建。
3.3 支付模块的跨平台处理与 IAP 适配
支付是另一个比较折腾的模块。Android/iOS 上很自然用微信支付、支付宝支付,但在鸿蒙上,微信和支付宝的 Flutter 插件兼容情况并不稳定,而且 HarmonyOS NEXT 有自己的应用内支付渠道——华为 IAP。
如果你接的是华为应用市场分发,我建议直接用 IAP 来处理虚拟商品类订单;如果包含线下实物或服务(比如照片打印服务),可以用华为支付或者微信/支付宝的鸿蒙 SDK,不过那要写原生桥接。
我在“附近自助照相馆”里,套餐属于服务类商品,最终选择在鸿蒙端通过华为支付 SDK 完成。做法很简单:
在 ohos 平台里新建一个PaymentBridge.ets,用 FeatureAbility 调起支付。Dart 侧通过 MethodChannel 调用:
class PaymentService { static const _channel = MethodChannel('app.photobooth/payment'); static Future<bool> pay(String orderId, double amount) async { final success = await _channel.invokeMethod('pay', { 'orderId': orderId, 'amount': amount, }); return success; } }鸿蒙原生侧关键代码:
import { BusinessError } from '@kit.BasicServicesKit'; import { payment } from '@kit.PayKit'; export function pay(orderId: string, amount: number): Promise<boolean> { return new Promise((resolve, reject) => { let request: payment.PayRequest = { amount: amount, productName: '自助拍摄套餐', requestId: orderId, }; payment.pay(request) .then(() => resolve(true)) .catch((err: BusinessError) => reject(err)); }); }需要注意,IAP 或华为支付都需要在“AppGallery Connect”里配置商品信息,而且支付成功后的回调一定不要只相信客户端返回结果,要求后端用服务端票据二次校验。我当时后端同事专门写了一个回调接口,App 支付成功后会把订单号和支付凭证一起传给后端,由后端向支付平台确认后才把订单置为成功,避免恶意绕过。
3.4 照片处理与上传
照相馆应用里,照片处理是核心体验。用户拍照后可能要裁掉无关背景、调节亮度、加个简单的日期水印,然后再上传打印。
Flutter 里我用的是image_picker拍照,crop_image做裁剪。考虑到自助拍照的场景,用户在包厢内大多是站立拍摄,所以裁剪时也支持了 5:7 的证件照比例。
上传方面,如果只是走 Flutter 的 http 库把文件以 multipart 方式传给服务器,在弱网环境下体验会很差。我建议使用 DIO 插件,支持队列、进度和断点续传。代码段:
final dio = Dio(); final formData = FormData.fromMap({ 'storeId': storeId, 'boxNo': boxNo, 'files': await Future.wait( images.map((path) async { return await MultipartFile.fromFile(path, filename: 'photo_$timestamp.jpg'); }), ), }); final response = await dio.post('/api/photo/upload', data: formData, onSendProgress: (count, total) { uploadProgress.value = count / total; }, );这里有个性能优化点:手机摄像头拍出来的图动辄 5MB、8MB,不处理直接传会很占带宽。我在上传前统一用 Flutter 的image库做了一次压缩,把最长边压缩到 1920px,质量压到 85%,既满足打印需求又让上传快了不少。压缩后的图平均只有 300KB,上传速度体验会好很多。
4. 踩坑实录与问题排查
4.1 构建与依赖问题(含 CMake、Gradle)
跨平台开发最崩溃的部分往往不是业务逻辑,而是环境。我整理了几个高频问题。
CMake error at CMakeLists.txt:3 (project): Generator Visual Studio 16 2019
这个问题出现在用 Flutter 构建一些包含原生 C/C++ 代码的插件时,而且多发生在 Windows 环境。默认 CMake 找不到合适的生成器。解决方法是显式指定生成器,或者在安装 Visual Studio 时勾选“使用 C++ 的桌面开发”工作负载。如果不想重装 VS,可以在 CMake 配置里加:
flutter config --cmake-generator="Visual Studio 17 2022"或者直接打开android/local.properties,添加:
cmake.generator=Visual Studio 17 2022Gradle 版本不一致导致依赖包拉不下来
Flutter 项目升级后,Gradle wrapper 版本和 AGP 版本很容易不匹配。我当时遇到的报错是“Could not find com.android.tools.build:gradle:7.2.0”。这种问题的处理思路很简单:先检查android/settings.gradle里 classpath 的 AGP 版本,再对照gradle-wrapper.properties里的 Gradle 版本。大版本对应关系可以查官方兼容表。
另外,国内网络环境下 Gradle 官方仓库极慢,建议在android/build.gradle里替换仓库为阿里云镜像:
allprojects { repositories { maven { url 'https://maven.aliyun.com/repository/public' } maven { url 'https://maven.aliyun.com/repository/google' } maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } mavenCentral() } }Flutter 各个版本之间依赖不兼容导致的 pub 拉取失败
这个问题在 Flutter 生态里很常见,某个插件只兼容 Flutter 3.10,但你用了 3.16,结果编译时报一堆 undefined class。我后来给自己定了个规矩:项目一开始就固定 Flutter 版本,用fvm做版本管理,每个平台目录也都单独锁版本。这个项目用的是 Flutter 3.16.9 + ohos 适配分支,所有插件都先验证过支持该版本才引入。
4.2 UI 与界面细节问题
showLicensePage 的主题颜色不跟随全局 Theme
Flutter 里自带的showLicensePage弹窗会显示开源许可证列表,但它的背景和字色在某些主题下不协调。这个问题的根源是 LicensePage 内部使用的是Theme.of(context)的默认值,如果在主页面套了自定义 Theme,它可能没拿到。最简单的做法是把它放到一个干净的MaterialApp子页面里,或者自定义一个入口页面。我最终在应用“关于”页里没走系统弹窗,而是自己用 ListView 渲染了 licenses,这样样式完全可控。
CheckboxListTile 文字距离按钮太近
这个问题在 Flutter 里特别经典,很多新手会问“文字离勾选框到底怎么调”。其实CheckboxListTile的间距由controlAffinity和dense以及title的 padding 共同决定。如果你想让右侧文字离左侧按钮远一点,可以这样处理:
CheckboxListTile( controlAffinity: ListTileControlAffinity.leading, contentPadding: EdgeInsets.only(left: 24, right: 24), title: Padding( padding: EdgeInsets.only(left: 12), child: Text('我已阅读并同意用户协议'), ), ... )多试几次,你就能摸清楚 contentPadding 和 title Padding 的组合。
鸿蒙安全区域与刘海屏适配
在鸿蒙真机上,默认页面会被系统状态栏遮挡一部分。解决方法是获取状态栏高度,给页面顶部加 padding。Flutter 里可以用MediaQuery.of(context).padding.top,但因为 Flutter 在 ohos 适配层的实现与 Android 不完全一致,我遇到过一次获取高度为 0 的情况。最终还是老老实实在 ohos 原生入口通过 avoidArea 拿到实际高度,再通过 MethodChannel 传给 Dart。
4.3 运行时权限与兼容性问题
相机权限名称不一致
同一个应用,Android 和鸿蒙的权限名称完全不一样,如果代码里硬编码了 Android 权限名,在鸿蒙上会拿到拒绝权限的结果。我统一封装了一个PermissionManager,根据Platform.isOHOS分发到不同的权限申请逻辑。检测逻辑用了permission_handler插件,但它在 ohos 上支持不完善,所以鸿蒙端我直接通过 MethodChannel 调用了abilityAccessCtrl的权限接口。
图片选择器在鸿蒙上无法打开图库
这个问题比较隐蔽。image_picker在 Android 上一般没问题,但鸿蒙上如果调用系统图库需要额外配置数据类型。排查时可以看到日志里有 “type mismatch” 之类的提示。绕开办法是使用鸿蒙原生PhotoViewPicker,通过 MethodChannel 拿到图片路径再传回 Dart。
遇到这些问题时,我的核心经验是:不要一头扎进 Dart 层反复调,很多运行时权限和系统 UI 的差异必须在原生层解决。跨平台应用的最后 20% 适配工作,往往就是这些碎片化的系统差异。把这些差异集中到一个 adapter 层,不要在业务代码里到处写 if/else,后面维护会轻松很多。
5. 优化与上线心得
5.1 包体积与启动性能优化
Flutter 应用比原生应用天生多一个引擎,包体积控制不好很容易超过 100MB,而应用市场对这个卡得越来越严。我在项目里做了几件比较有效的事:
- 移除未用插件:用
flutter pub deps检查依赖树,把没有实际引用的插件全部删掉。尤其是一些项目初期为了实验加的地图、动画插件,最后竟然帮我把打出来的 APK 体积从 82MB 降到了 64MB。 - 禁用未使用的渲染特性:在
pubspec.yaml里开启--tree-shake-icons,同时保证所有 icon 都从 IconData 引用,不使用整包 MaterialIcons 字体。 - 启动流程异步化:把定位、登录态刷新、远程配置拉取都放到首帧之后,用
WidgetsBinding.instance.addPostFrameCallback去做,让首界面能先画出来。
实测下来,我的中端测试机冷启动时间从原来的 1.8s 优化到 1.2s,虽然不算极致,但用户感知已经明显提升。
5.2 鸿蒙应用打包与上架注意事项
鸿蒙应用最终上架的不是 APK,而是 HAP。在 DevEco Studio 里配置好签名之后,可以执行:
hvigorw assembleHap生成的 HAP 位于ohos/entry/build/default/outputs/default/entry-default-signed.hap。上架华为应用市场前要注意:
- 应用分类:涉及地图、支付等功能,隐私政策里必须明确说明目标 SDK 和权限用途。
- 备案要求:无论是安卓还是鸿蒙,上架前都需要完成相应的 App 备案,不要想当然以为有出版号就行。
- 鸿蒙和安卓不能共用一个签名证书,需要分别申请。证书申请流程说快也快,说慢也慢,建议在项目开发中期就开始,别等到打包上架那一步才去催证书。
5.3 后续扩展方向
这个应用后续还有很多可以延展的地方,我在当前版本里只做了基础闭环,但架构上留了位置:
- AI 换装与智能修图:可以利用大模型能力做一键背景更换,这个在拍照套餐里很受欢迎。
- 会员体系:充值送优惠券、月度套餐、多人拼单,能有效提高复购。
- 设备远程监控:店内摄像头画面预览、设备故障告警,虽然对 C 端用户不可见,但是运营端的高频需求。
- 跨品牌聚合:如果聚合了多家自助照相馆,地图列表和订单体系要设计成多商家模型,后端加一层商家维度就够了。
这些扩展的方向,本质上还是建立在 Flutter 跨平台能力之上。鸿蒙市场刚刚开放,对 Flutter 开发者来说,先跑通一套完整业务链路,形成自己的适配知识库,后面再做类似项目会越来越顺手。
最后分享一个我自己感触很深的细节:跨平台开发的幸福感,不是来自一套代码到处跑的光环,而是来自你敢于面对各平台差异、提前把适配层做干净的那份踏实。这个项目里我花了差不多三分之一的时间在环境配置和原生桥接上,但那些踩过的坑最后都变成了团队的资产。如果你正在做 Flutter + 鸿蒙的路上,别焦虑,照着这个流程理顺,你也能跑通。