ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flutter鸿蒙应用开发实战:从跨平台原理到石料管理工具落地

Flutter鸿蒙应用开发实战:从跨平台原理到石料管理工具落地 最近我在折腾一个小项目用 Flutter 写一个篆刻石料记录应用目标是一套代码同时跑在 Android、iOS 和鸿蒙设备上。项目本身不算大但价值在于这是我第一次把 Flutter 的跨平台能力延伸到鸿蒙系统上走完整条链路。如果你也在关注 Flutter 跨平台开发、鸿蒙应用开发或者只是想找一个“不算复杂但足够真实”的练手项目这篇教程应该对你有用。先说说这个应用解决什么问题。篆刻圈子里有一个特别实际的痛点石料一多就乱。什么品种、什么尺寸、从哪里买的、刻到哪一步了、有没有边款稿全靠脑子记真的不靠谱。我见过有朋友用 Excel 记账本式的表格管理石料也有直接在手机备忘录里贴照片的但都不够顺手。这个应用就是把每一块石料变成一条结构化记录带照片、带标签、带搜索打开手机就能查“我那块 2.5 厘米的巴林冻石到底收在哪了”。选 Flutter 来做这件事理由很直接我不需要为 Android 和鸿蒙各写一套 UIDart 写一遍两边都能编译运行。如果你正好在纠结怎么入门鸿蒙开发又放不下已有 Flutter 技术栈这篇文章应该能给你一个比较完整的参考答案。下面我从需求拆解讲到数据建模再到鸿蒙适配和代码实操最后把踩过的坑整理成清单你不妨照着走一遍。1. 先聊清楚这个应用到底要做什么1.1 篆刻石料记录的真实使用场景篆刻用的石料常见的有青田、寿山、巴林、昌化这几大类每一个大类下面还能细分出很多小品种。价格差异极大从几十块钱的练习石到几千上万的冻石都有。尺寸从最小的 1.5 厘米印章料到五六厘米的闲章料甚至还有不规则的自然形。这些信息对一个玩篆刻的人来说每一笔都是值得记的。但“记录”这件事难点从来不是记下来而是以后怎么找出来。比如你某天在店里看到一块很喜欢的老挝石想对比一下家里有没有同尺寸同品类的如果全靠翻相册、翻聊天记录那就太痛苦了。所以这个应用的核心逻辑有三条录入要快、信息要全、检索要准。我甚至把常用的尺寸和品种做成了下拉选项就是为了减少打字。这个场景其实和“收藏品管理”或者“仓库台账”非常像数据模型的设计思路是可以直接复用的。你做别的工具类应用比如胶片相机镜头管理、酒柜存酒记录这套结构基本上换皮就能用。1.2 技术选型为什么是 Flutter以及鸿蒙的适配现状先说 Flutter 本身。Flutter 和 React Native、Weex 这类框架最大的区别在于 UI 不是调用系统控件而是自己用 Skia 或者 Impeller 引擎直接绘制。这意味着同一套代码在不同系统上渲染出来的效果几乎一模一样跨平台表现非常稳定。对工具类应用来说这种稳定比“像素级还原设计稿”更重要。再说鸿蒙。OpenHarmony 和华为 HarmonyOS NEXT 的生态还在高速迭代期原生开发推荐的是 ArkTS 语言和 ArkUI 框架开发工具是 DevEco Studio。如果你是从零开始学鸿蒙原生也不是不行但如果你已经积累了不少 Flutter 代码直接用 Flutter 适配层往鸿蒙上搬投入产出比明显更高。目前社区已经有比较成熟的 Flutter 鸿蒙适配仓库原理上就是把 Flutter 引擎编译产物打包进鸿蒙的 HAR 包里再在 Ability 的界面容器里挂载 Flutter 视图。网上搜“开源鸿蒙 flutter”能看到不少相关讨论。我还注意到最近 Electron 移植鸿蒙、Tauri 2 跑鸿蒙这类话题也很热。Electron 和 Tauri 本质是 WebView 方案把网页应用套个壳跑在鸿蒙上开发成本低但性能和系统能力调用的深度都有限。Flutter 是编译成原生代码运行引擎自绘 UI在交互流畅度和原生能力对接上都要强一截。我做这个石料记录应用选 Flutter就是看中它能平衡开发效率和最终体验。1.3 功能清单第一版只做四件事任何一个项目上来就想做成“完美术管理软件”基本都会烂尾。我给自己划的第一版功能非常克制只有四块石料列表按最近更新时间倒序卡片式展示缩略图、品种、尺寸和状态。石料详情与编辑查看完整信息支持修改、补图、删记录。图片采集调用系统相机拍照或者从相册选择支持压缩处理。搜索与筛选按关键词模糊搜按状态和品种过滤。第二版我预留了两个方向导出 JSON/CSV 备份以及边款图、印面布局图的单独管理。第三版想加一个简单的刻制定期提醒比如“这块石头放太久了拿出来练练手”。版本规划的意义在于你永远可以用最小的成本验证核心流程有没有问题后面再慢慢加东西。2. 数据模型与本地存储设计2.1 石料信息表应该怎么建这个应用的数据模型是整个项目的基石。我建了一张stones表字段设计如下字段类型说明idINTEGER PRIMARY KEY AUTOINCREMENT主键nameTEXT自定义名称比如“巴林冻石-朱文印”varietyTEXT品种青田、寿山、老挝等originTEXT产地或来源店铺length / width / heightREAL尺寸单位 cmweightREAL重量单位 g允许为空statusINTEGER状态0未刻 1设计稿 2刻制中 3完成purchase_dateTEXT购入日期ISO 格式purchase_priceREAL购入价格可为空tagsTEXT逗号分隔的标签比如“练手石,送礼”notesTEXT备注可写边款内容、灵感记录image_pathTEXT封面图绝对路径created_at / updated_atTEXT时间戳有几个字段值得特别注意。status我用整数而不是字符串是因为状态是要做筛选和统计的整数映射的枚举在代码里更好维护。tags字段虽然违反了“一张表一个字段存一个值”的规范但对移动端轻量场景来说逗号分隔存储的查询压力可以接受而且实现最简单。如果你以后要做标签云或者多标签组合筛选再拆一张关联表也不迟。purchase_date我建议不存时间戳格式直接存 ISO 字符串更直观SQLite 的字符串排序对 ISO 日期天然友好。2.2 为什么用 SQLite 而不是直接写 JSON 文件有的朋友可能会问就记录石料这么点数据直接写 JSON 文件不就好了吗一开始我也这么想但实际做下去发现查询场景太复杂了。你要按品种过滤又要按尺寸区间过滤还要按状态统计纯手动操作 JSON 数组写这套过滤逻辑代码又臭又长。SQLite 一条SELECT * FROM stones WHERE status 0 AND width 2.0 AND width 3.0就搞定了。移动端用 SQLite 是再常规不过的选择。Flutter 生态里最常用的就是sqflite插件API 封装得比较简单支持事务支持原生 SQL。我的做法是在应用启动时初始化数据库建表然后封装一个StoneRepository类所有对stones表增删改查的操作都走这个类。这样页面层不直接碰 SQL以后换数据库或者加字段页面代码基本不用改。调试的时候我强烈推荐一个桌面工具DB Browser for SQLite也就是网上常说的 db4s。它是个开源跨平台的 SQLite 数据库管理工具直接打开模拟器导出的.db文件就能看表结构、跑 SQL、改数据。我排查过好几次“为什么列表页少显示一条记录”的问题都是在 db4s 里跑了一遍 SQL 才发现是筛选条件写错了而不是代码 bug。2.3 图片存储策略存路径别存 BLOB图片存储是我特别想强调的一点。很多人第一次做带图应用顺手就把图片二进制塞进数据库 BLOB 字段。这在小数据量时没问题但图片一多数据库体积会迅速膨胀备份、同步、迁移全都变慢。正确做法是图片文件存磁盘数据库里只存一个绝对路径。我的目录结构是这样规划的/storage/emulated/0/Android/data/包名/files/seal_stones/下面按石料 ID 建子目录比如seal_stones/12/cover.jpg、seal_stones/12/extra_1.jpg。这样备份的时候只需要拷整个seal_stones目录非常干净。还有一个容易被新手忽略的问题图片压缩。手机拍一张照片原图动辄 3 到 5 兆直接存进去以后导出备份就是一场灾难。我录入图片的时候会用flutter_image_compress压缩一遍长边控制在 1920 像素质量参数选 85肉眼几乎看不出差别体积能压到原图的五分之一甚至更小。3. 鸿蒙平台适配从工程搭建到渲染引擎3.1 开发环境准备与工程目录结构如果你想自己动手跑一遍环境准备是第一步。Flutter 这边就按常规来用 Android Studio 的 New Project 向导创建 Flutter 项目这一步网上搜“如何 as 创建 flutter 项目”有大量教程不再赘述。关键点是项目创建之后你会在根目录看到android/、ios/、lib/这几个标准目录。鸿蒙适配要做的事情是在这个工程里再增加一个鸿蒙原生工程目录。鸿蒙侧需要准备的开发环境是 DevEco Studio 和 OpenHarmony SDK。Flutter 跑鸿蒙的基本思路是Flutter 引擎作为依赖库打进鸿蒙工程鸿蒙的 Ability 作为宿主容器在页面上挂一个 Flutter 渲染视图。整个鸿蒙工程可以理解成一个壳工程它负责把 Flutter 引擎拉起来UI 全部由 Flutter 侧渲染。我踩过的第一个坑就是版本对应关系。Flutter SDK 版本和鸿蒙适配仓库的版本必须严格对应不能想当然拿最新的 Flutter 稳定版去配一个几个月前的适配层编译报错会让你怀疑人生。建议直接看适配仓库的 README它通常会明确写“本适配版本对应 Flutter x.y.z”。这个环节没有任何捷径老老实实按文档锁版本就对了。3.2 理解混合工程集成从“flutter aar”说起网上关于“flutter aar”的讨论很多这个问题的本质是把 Flutter 模块打包成 Android 的 AAR 库再混入原生工程。鸿蒙侧做的其实是同一件事只是产物从 AAR 变成了鸿蒙的 HAR 或者动态库。我第一次理解这个概念的时候把 Flutter 想象成一台游戏机鸿蒙的 Ability 是电源插座Flutter 引擎是游戏机本身Dart 代码是插进去的游戏卡带。主机鸿蒙应用还是你自己的但插上卡带之后画面和玩法都是 Flutter 说了算。混合工程的集成顺序我强烈建议这样来先跑通一个最简单的 hello worldAbility 创建时就拉 FlutterViewFlutter 侧显示一屏文字确认双向链路正常。然后第二步再测试 MethodChannel原生和 Flutter 互相调一个方法。最后第三步才往里面加真正的业务页面。一上来就接 PlatformView、接复杂插件出了问题你根本不知道是引擎问题还是代码问题。3.3 PlatformView 与原生能力桥接鸿蒙适配的重头戏石料记录应用要用到相机和相册这是系统级能力。问题在于Flutter 社区里成熟的插件比如image_picker可能没有现成的鸿蒙平台实现。这时候你有两条路可走要么用 MethodChannel 自己写桥接调到鸿蒙原生 API要么参考插件的 platform interface 机制为鸿蒙平台补一个实现类。我实际做的时候两条路都走了。image_picker这种成熟插件先查它有没有鸿蒙实现没有就用自己的 MethodChannel。这里有一个非常关键的坑点PlatformView 在鸿蒙适配层的创建时机。Flutter 侧创建原生视图的请求发过去之后鸿蒙侧如果没在正确的生命周期节点上插入原生视图页面切换的时候就会出现黑块或者闪烁。我最后用“延迟加载 视图容器复用”解决了这个问题页面切回来的时候不会重新创建整个原生视图只更新内容数据。“flutter platformview”这个关键词搜索热度一直很高说明大家确实都在跨端开发里被原生视图折磨过。我的体会是不要试图在 PlatformView 里塞太多交互控件能用 Flutter 自己绘制的部分就自己绘制双方交互越少越好这是一个工程折衷。3.4 Impeller 渲染引擎鸿蒙上的潜在性能红利Flutter 3.10 之后iOS 平台默认启用 Impeller 渲染引擎Android 也在逐步切换。它解决的是 Skia 在部分设备上着色器编译导致的首帧掉帧、卡顿问题。对 Flutter 鸿蒙适配来说Impeller 的意义在于如果适配版本支持 Impeller那么鸿蒙设备上的滚动流畅度、动画顺滑度都会有明显提升如果不支持运行期碰到复杂动画时可能触发一次性着色器编译画面会卡一下。石料列表页有大量图片和卡片滚动非常频繁。我在鸿蒙适配版本上专门验证过 Impeller 的开关情况。没有 Impeller 的时候快速滚动列表偶尔会有几帧掉帧开启之后明显感觉丝滑了。如果你在做别的动画场景更复杂的 Flutter 鸿蒙应用一定要去确认适配层是否支持 Impeller并在初始化时显式配置不要靠默认值。4. 核心功能实现与组件通信4.1 页面结构与底部导航设计这个应用的主界面用三个 Tab 页组成石料列表、筛选搜索、个人设置。放在 Flutter 里最直接的做法就是用BottomNavigationBar加索引切换页面一套代码在 Android 和鸿蒙上效果一样好。热搜词里“鸿蒙应用开发底部导航栏”的搜索量很高但如果你用的是 Flutter这个需求反而是最省心的因为 Flutter 已经把底部导航做成了最基础的组件你不需要关心鸿蒙的 ArkUI 怎么实现。三个页面的职责划分要清晰。列表页负责展示和跳转筛选搜索页负责构建查询条件和展示结果设置页负责备份、恢复、版本信息等低频操作。我见过不少新手把筛选功能塞进列表页的顶部结果一个页面越写越长最后自己也维护不动了。独立页面还有一个好处搜索页的搜索框可以获得独立的焦点管理不会跟列表页的滚动手势抢事件。4.2 组件通信从“人传人”到统一状态管理“flutter组件通信”是高频搜索词说明这是很多初学者跨不过去的坎。组件通信本质上就三件事父传子、子传父、跨层传。石料记录应用里这三个场景全都涉及。父传子最简单构造函数直接传参。子传父稍微绕一点需要把回调函数作为参数传下去比如列表项组件点击时回调onTap(int stoneId)。真正麻烦的是跨层通信比如编辑页保存了一块石料列表页需要刷新设置页里的统计数字也要跟着变这时候一层层回调就会把代码搞得像意大利面条。我最后选用的是 Provider ChangeNotifier 这套方案。石料仓库类继承ChangeNotifier暴露addStone、updateStone、deleteStone等方法方法内部操作数据库之后调用notifyListeners()。列表页监听这个类数据一变它就重建完美解决跨页面刷新问题。这个方案的优点是理解成本低没有事件总线的“幽灵消息”问题调试时可预测性非常强。顺手回答一个搜索热词里很多人困惑的问题“flutter future的then回调是放入微任务队列吗”。答案是Dart 的 Future 回调确实会进入微任务队列Microtask queue在当前同步代码执行完之后、下一个事件之前执行。这直接决定了then回调的时序。实际编码中你会在编辑页await保存操作然后Provider.of去取最新数据注意到 UI 不会立刻刷新因为notifyListeners()触发的重建是放到微任务里跑的页面会在当前帧结束后统一刷新。理解这一点对排查“为什么数据改了界面不变”特别有帮助。4.3 图片选择、压缩与 EXIF 旋转问题图片选择用image_picker插件Android 上调用系统相机和相册是常规操作。鸿蒙平台上我在适配层补了一个 MethodChannel 实现选择完图片之后返回本地文件路径。这里的接缝点在于Flutter 层拿到的路径是鸿蒙侧传过来的格式和 Android 原生的content://或者/storage/emulated/0/路径不同需要统一转换成绝对路径再使用。压缩代码核心就几行final compressed await FlutterImageCompress.compressWithImage( File(imagePath).readAsBytesSync(), quality: 85, minWidth: 1920, format: CompressFormat.jpeg, );但这里有一个特别隐蔽的坑EXIF 旋转信息。手机拍竖图很多相机会把旋转信息写进 EXIF图片本身的像素是横着的。如果压缩库重新编码 JPEG 时丢掉了 EXIF 旋转信息图片就会显示成横着的。这不是 Shutter 级的大问题但对用户来说非常碍眼。解决办法是先读取原图的 EXIF 旋转角度如果为 90 或 270就先用图片编码库旋转回来再压缩。我的处理函数里这段逻辑大概占了二十行但避免了所有竖拍图片横躺的尴尬。4.4 搜索与筛选SQL 动态拼接的技巧搜索的核心是一句 SQL但条件需要动态拼接。用户可能只输关键词也可能关键词加状态再加品种全选。最稳妥的做法是用 Map 构建查询条件避免手动拼字符串导致 SQL 注入。FutureListStone search({String keyword , int? status, String? variety}) async { final where String[]; final args Object?[]; if (keyword.isNotEmpty) { where.add((name LIKE ? OR variety LIKE ? OR tags LIKE ?)); final kw %$keyword%; args.addAll([kw, kw, kw]); } if (status ! null) { where.add(status ?); args.add(status); } if (variety ! null variety.isNotEmpty) { where.add(variety ?); args.add(variety); } final sql SELECT * FROM stones WHERE ${where.join( AND )} ORDER BY updated_at DESC; return db.query(stones, where: where.join( AND ), whereArgs: args); }搜索结果的即时刷新有一点要注意TextField 的onChanged回调触发频率非常高每次输入都会触发 SQL 查询。我给搜索请求加了一个 300 毫秒的防抖也就是说用户在停手 300 毫秒之后才会真正发起查询。这个体验上的细节比很多人想象的更重要实际用下来输入过程非常跟手。5. 实操问题排查与避坑清单5.1 flutter 新建项目后跑不起来先别急着怪代码搜索热词里有一条特别扎心“flutter新建项目后 跑不起来”。这个问题的概率远比你想象的高但你遇到的 90% 情况其实和业务代码无关。我的排查路径是固定的先跑flutter doctor看环境状态再看 Gradle 下载是否失败因为国内网络环境下 Gradle 发行版经常下不动最后确认 Android SDK 的 Build-Tools 版本是不是被 Android Studio 更新搞丢了。另外一个高频报错是e/flutter ... dart_vm_initializer.cc unhandled报错信息指向 Dart VM 初始化器。这个错误通常是设备或模拟器的 CPU 架构和 Flutter 引擎产物不匹配或者引擎启动参数有问题跟你的 Dart 代码没什么关系。遇到这附近的问题先换一个模拟器试试再看适配版本的构建配置不要一头扎进业务代码里找原因。5.2You are applying Flutters main Gradle plugin imperatively报错这条报错的完整提示是“You are applying Flutter’s main Gradle plugin imperatively using the apply method”。新版本 Flutter 已经把 Gradle 插件改成声明式应用方式但老项目模板或者网上拷贝的旧配置还会用apply plugin:这种命令式写法两者一冲突就报错。修复方式是把android/settings.gradle里的插件管理方式改成 plugin DSL或者直接把项目模板升级到新版。我给你的建议是遇到 Gradle 相关报错第一反应不要抄网上两年前的老代码而是打开 Flutter 版本对应的 changelog看看这个版本的构建系统发生了什么变化。你在这上面花十分钟看的文档可能比搜两小时解决方案更能解决问题。5.3 新增石料后列表不刷新微任务队列的陷阱这是一个实际发生过的 bug。我在编辑页保存石料成功后调用Navigator.pop返回列表页但列表没有刷新。排查之后发现问题出在保存操作的时序上。save方法内部先 await 了数据库插入然后notifyListeners()再返回。但调用方在 await 之后立刻pop页面的build方法跑到一半Provider 的监听还没来得及处理视觉上就是“保存了但没刷新”。解决办法是把状态变更和 UI 导航分离。保存数据之后不要立刻pop而是先等 Provider 的notifyListeners()执行完帧回调再执行导航。实操中我用了一个最简单有效的方式在编辑页的按钮回调里先await addStone()然后在下一个微任务里执行pop。理解了 Dart 的事件循环你就能解决这一类看着很奇怪的状态问题。5.4 数据迁移与备份不要删库重来版本迭代必然涉及表结构变更比如第二版我想给石料表增加一个“耗材成本”字段。sqflite 的onUpgrade回调就是干这件事的它会携带oldVersion和newVersion你在这个回调里执行字段增量变更保留已有数据。新手最容易踩的坑是图省事直接 DROP TABLE 再重建用户的存量数据连个招呼都不打就没了这种体验基本等于劝退。备份功能还有一个平台差异需要考虑Android 11 之后应用外部文件的路径访问权限收紧了很多。网上大量老教程里写的“直接往 /sdcard/ 下写文件”现在已经不可行。我的备份方案是把数据库文件先复制到应用的外部私有目录然后调用系统分享组件让用户选择保存位置比硬编码一个绝对路径要稳妥得多也避免了沙箱权限踩雷。整个项目做下来我最大的体会是Flutter 跨鸿蒙这条路已经可以走了但你还得提前做好心理建设——适配层时不时会给你挖坑社区资料也比 Android 那边少。只要主线思路清晰数据模型先想透再一步步把功能填进去这套方案就能真正变成你自己的跨平台产品能力。遇到 PlatformView 闪烁、Gradle 报错、图片旋转这类问题回来翻翻这篇教程里的排查思路应该能帮你省下几个晚上的时间。
返回列表