篆刻这行有个很现实的问题:刻刀和石头都好说,但“记录”这件事一直很原始。石料从哪里来、什么品种、多大尺寸、切出过几块料、刻到第几步,多少人还在用本子和脑子在记。松散的纸质记录换个地方就丢了,手机相册里的照片过几个月根本对不上号。我接了个小需求,就是给一个篆刻工作室做石料档案管理,要求一个App能同时装在安卓、Windows和鸿蒙设备上。最后这个项目是用 Flutter 跨平台方案落地的,过程中踩了不少适配的坑,尤其是鸿蒙这块。这篇内容就把整个开发流程拆开讲,从为什么选 Flutter,到数据模型怎么设计,再到鸿蒙适配和常见报错,都是实际动手验证过的东西。
这个教程适合两种人:一种是会用 Flutter 但想试试鸿蒙适配的开发者,另一种是完全没有移动端基础、想给自家小作坊做个工具类App的手艺人。不需要你有多深的编程底子,但最好有基本的 Dart 语法认知。文中的代码片段可以直接抄,但更重要的是后面那些“为什么这么写”的逻辑。
1. 项目整体设计与技术选型思路
1.1 为什么最终选了 Flutter 而不是其它跨端框架
跨端开发的选择,说白了就是三选一:自绘引擎、原生桥接、Web 容器。React Native 走的是桥接原生控件的路子,思想没问题,但每次原生升级都可能带来适配成本;Electron 和 Tauri 这类方案本质上是把 Web 页面包进桌面壳里,开发效率高,但移动端性能和内存占用不理想,在手机上跑一个小工具App总觉得笨重。
Flutter 和它们都不太一样。Flutter 用 Skia/Impeller 引擎自己绘制每一个像素,不依赖系统原生控件。这意味着同一套 UI 代码在安卓、iOS、Windows、Linux、鸿蒙上渲染出来的效果几乎是一致的。做篆刻记录这种工具类App,界面不需要多华丽,但不同设备上看起来不能有“换了个人做”的割裂感。Flutter 最打动我的一点是:你在电脑上调试好的布局,到手机上依然是你调试的那个样子。
另外,Flutter 的单代码库对这类小型项目特别友好。一个石料记录应用撑死也就是三五个页面加一个本地数据库,用一个主力语言 Dart 写 UI、写业务、写数据库操作,比“前端写一套 + 安卓写一套 + Windows 再写一套”省下的人力不是一点半点。团队如果只有两三个人,这是最现实的选择。
1.2 鸿蒙为什么值得纳入目标平台
鸿蒙系统这几年在手机、平板、智能设备上的覆盖率越来越高,哪怕只是作为一个新增的发型渠道,也值得纳入目标平台。对于篆刻工作室这种线下生意,客户很可能是华为手机用户,你要是不支持鸿蒙,那一部分用户就只能干瞪眼。
从技术层面讲,Flutter 适配鸿蒙的路径已经走通了。OpenHarmony 社区维护着 Flutter 的分支实现,让 Flutter 引擎能够跑在鸿蒙设备上,打包产物就是鸿蒙标准的 hap 安装包。你在 Flutter 工程里加一个 ohos 平台,剩下的开发模式和你写安卓端几乎一模一样:页面照常写、插件能复用就复用,遇到缺失的原生能力用平台通道补上。这也是我敢接这个需求的原因——成本是可控的,不是从零开始学一套 ArkUI。
需要提前说明的是:Flutter 上鸿蒙和原生 ArkUI 开发不是二选一的替代关系。如果你只做鸿蒙一个平台,直接学 ArkUI 更顺;但如果你要同时覆盖多个终端,Flutter 这套“一套代码、多端分发”的模式,性价比就体现出来了。
1.3 篆刻石料记录应用的核心需求拆解
写代码前先把需求想明白,比写代码本身重要得多。我跟工作室老板聊完,整理出的核心需求其实很朴素:
| 模块 | 功能说明 | 优先级 |
|---|---|---|
| 石料档案 | 记录名称、品种、产地、购入日期、尺寸、重量、纹理特征、初始照片 | 高 |
| 图片管理 | 原石、打坯、细雕、成品等多阶段拍照,支持压缩和缩略图 | 高 |
| 刻制进度 | 状态流转:待刻、打坯、细雕、抛光、完工,附带每次操作的备注 | 高 |
| 检索筛选 | 按品种、产地、当前状态、关键词组合筛选 | 中 |
| 数据备份 | 数据库一键导出,换设备不丢档 | 中 |
| 本地优先 | 所有数据存在本机,不依赖网络,断网也能用 | 高 |
这个需求清单有一个隐含的架构决定:数据库用本地 SQLite,不做服务端。工作室的实景环境经常在地下室或库房,网络不稳定,让师傅们等云端同步不现实。本地数据库单文件存储,再用导出功能定期备份,反而最可靠。这个“本地优先”的思路贯穿了整个项目,后续所有选型都围绕它展开。
2. 从零搭建工程:目录、分层与组件通信
2.1 用 Android Studio 创建 Flutter 工程时最容易忽略的事
网上搜“如何 AS 创建 Flutter 项目”,出来的教程大同小异:装插件、新建工程、等编译。但实际动手时有几个细节特别容易坑人。
首先,Flutter SDK 和 Android Studio 的版本必须对得上。建议先跑一遍flutter doctor把环境问题全部清干净,再开始建项目。我看到过太多人跳过这一步,最后卡在各种 SDK 缺失的报错上浪费一下午。
创建工程时有一个 Platform 勾选界面,默认勾了 Android 和 iOS。做鸿蒙适配的话,如果你用的是社区适配版 Flutter(支持 ohos 平台),这里会多出一个 OHOS 选项,记得勾上。命令行的创建方式也可行:
flutter create --org com.sealstudio --project-name seal_archive --platforms android,ios,ohos .注意--org要倒着写域名,这决定了应用包名,后面改起来很麻烦。项目名用下划线命名,不要用驼峰,Dart 包名规范不允许。
另外,工程路径里不要出现中文和空格。我见过一个唐诗App项目放在C:\用户\桌面\篆刻\下,编译时各种奇怪报错,把路径改成纯英文后问题就消失了。
2.2 工程目录怎么分,数据才不乱
很多 Flutter 新手把一个应用全部塞进 main.dart,页面、数据、工具函数堆在一起,两三千行之后改一个字段得全局搜索。做这个小项目我用了按 feature 分层的结构:
lib/ main.dart // 入口 app.dart // MaterialApp 与全局主题 core/ database/ app_database.dart // 数据库初始化与表结构 utils/ date_utils.dart features/ archive/ models/ seal_stone.dart // 石料数据模型 pages/ archive_list_page.dart archive_detail_page.dart archive_edit_page.dart widgets/ stone_card.dart archive_controller.dart // ChangeNotifier 状态管理 photo/ picker/ photo_picker.dart viewer/ photo_viewer.dart shared/ widgets/ empty_view.dart confirm_dialog.dart这个结构的核心思想是:每个业务模块(archive、photo)内部自己管自己,跨模块需要的东西通过独立文件或依赖注入传入。core 里放不依赖业务的基础能力(数据库、工具函数),shared 里放通用组件。这样做的好处是后期换数据库、加新功能,不会牵一发动全身。
我特别想把一个经验分享出来:数据模型和页面文件分开。石料档案这个模型在列表页、详情页、编辑页都会被使用,如果模型文件散落在各个页面目录里,引用关系会很乱。模型统一放在features/archive/models/下,页面只管调用,能省掉很多不必要的 import 错误。
2.3 组件通信与状态管理:小项目别想太复杂
Flutter 组件通信是个老话题,面试还常考。但落到这个项目上,思路很简单:先想清楚哪些数据是局部的,哪些是全局的。
| 通信方式 | 适用场景 | 本项目用途 |
|---|---|---|
| 构造参数传值 | 父→子单向传递,页面跳转带参数 | 列表页→详情页传入石料 ID |
| 回调函数 | 子→父传事件 | 删除确认弹窗、列表项点击 |
| InheritedWidget | 数据从根部向下共享 | 一般不直接用,Provider 底层用 |
| Provider | 跨页面共享可变状态 | 石料列表刷新、筛选条件共享 |
| Bloc/Riverpod | 复杂异步状态流 | 本项目没用,杀鸡不用牛刀 |
最终选了 Provider + ChangeNotifier。原因很简单:石料列表在列表页展示,但编辑页保存之后列表页需要刷新,这种“跨页面的状态同步”用 ChangeNotifier 非常顺手。
class ArchiveController extends ChangeNotifier { List<SealStone> _stones = []; List<SealStone> get stones => List.unmodifiable(_stones); Future<void> load() async { _stones = await AppDatabase.instance.getAllStones(); notifyListeners(); } Future<void> saveStone(SealStone stone) async { await AppDatabase.instance.insertStone(stone); await load(); notifyListeners(); } }然后在 widget 树顶部用ChangeNotifierProvider包一层,任何页面用context.watch<ArchiveController>()就能拿到最新的列表数据,并且自动触发 rebuild。这套模式对中小型 Flutter 应用来说,是性价比最高的选择。
3. 核心功能拆解:从一块“石头”到一本“档案”
3.1 石料数据模型与本地数据库选择
先定义数据模型。石料档案的字段不能拍脑袋,要覆盖工作室实际关心的信息:
class SealStone { final int? id; final String name; // 名称,比如“青田封门青” final String variety; // 品种分类 final String origin; // 产地 final double weight; // 重量(克) final double length; // 尺寸:长 final double width; // 尺寸:宽 final double height; // 尺寸:高 final String texture; // 纹理/特征描述 final String status; // 当前刻制进度状态 final DateTime boughtDate; // 购入日期 final String coverPhotoPath;// 封面原石照片路径 final String notes; // 备注 }数据库直接选了 SQLite,理由我在需求拆解时说过:本地单文件、稳定可靠、备份就是一个文件拷贝。Flutter 侧用的是 sqflite 生态,鸿蒙端用社区适配的实现,API 基本兼容,切换成本很低。
建表语句如下:
CREATE TABLE stones ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, variety TEXT, origin TEXT, weight REAL, length REAL, width REAL, height REAL, texture TEXT, status TEXT DEFAULT '待刻', bought_date TEXT, cover_photo_path TEXT, notes TEXT );这里有一个重要细节:日期字段不要存成时间戳整数,直接存 ISO 字符串。时间戳可读性差,而且日后跨平台格式化时还可能有时区偏差,ISO 字符串排序、显示都更友好。
开发调试时我强烈建议用 DB4S 这个开源工具直接打开 SQLite 文件查数据。App 运行后把数据库文件导出到电脑,用 DB4S 打开,表结构和数据一览无余,比写在代码里的调试日志直观太多。而且 DB4S 是跨平台的,Windows、macOS、Linux 都能用,配合 Flutter 开发恰好合适。
3.2 拍石料照片:路径、压缩与权限
拍照这块流程看着简单:点按钮、调相机、存照片、显示。实际上有三个坑:权限声明、路径选择、图片压缩。
拍照或选相册我用了 image_picker 生态,这个插件的问题在于返回的是临时缓存目录里的文件,不及时处理会被系统清掉。正确的做法是把图片复制到应用的文档目录下,再删除临时文件。
Future<String> pickAndSavePhoto() async { final picker = ImagePicker(); final XFile? picked = await picker.pickImage( source: ImageSource.camera, imageQuality: 85, maxWidth: 1920, ); if (picked == null) return ''; final appDir = await getApplicationDocumentsDirectory(); final fileName = 'stone_${DateTime.now().millisecondsSinceEpoch}.jpg'; final savedPath = '${appDir.path}/$fileName'; await File(picked.path).copy(savedPath); return savedPath; }压缩参数是实测出来的:原图动辄 5MB 以上,存储空间倒是小事,关键是列表页加载缩略图时会反复解码大图,内存肯定吃不消。我在拍照时用maxWidth: 1920配合imageQuality: 85做一次压缩,单张压到 300KB 左右,画质对展示足够。
权限是个大坑。Android 上要在 AndroidManifest.xml 里写相机权限;鸿蒙上要改module.json5,声明类似ohos.permission.CAMERA的权限;而且运行时权限弹窗的逻辑两个平台也不完全相同。适配时我的经验是:把权限请求放到页面初始化时主动触发,不要等到用户点拍照才弹窗,减少一步的慌乱操作导致的拒绝。
3.3 检索、筛选与输入防抖
石料档案一多,检索功能就要顶上来。这个项目的检索条件有三个:关键词模糊搜索(匹配名称、纹理、备注)、品种下拉筛选、状态筛选(待刻/打坯/细雕/抛光/完工)。刚开始我写的是每次输入框变化就立刻查数据库,结果非常卡——因为拍照多了之后数据库里有几百条记录,频繁模糊查 LIKE 对移动端来说还是有压力的。
解决方案是加一个防抖:停止输入 400 毫秒后再执行筛选。
Timer? _debounce; void onSearchChanged(String keyword) { if (_debounce?.isActive ?? false) { _debounce?.cancel(); } _debounce = Timer(const Duration(milliseconds: 400), () { controller.filter(keyword: keyword, variety: _selectedVariety, status: _selectedStatus); }); }为什么用 Timer 而不是把查询丢到 Future 里就算完?因为用户连续输入时,之前的查询很可能还没返回新结果就又被触发了,产生竞态大脑。防抖的本质是合并高频事件,只保留最后一次,对这类输入场景是正确解法。
筛选条件如果很多,不要每一个都单独 setState,可以把所有筛选条件做一个Filter对象,controller 里监听这个对象的变化,一次通知刷新。省代码,也避免多次重建列表。
3.4 列表页、详情页与下拉刷新实践
列表页是全 App 的门面,我用ListView.builder渲染石料卡片,每个卡片左上角是缩略图、右侧是名称和状态标签。状态标签用了不同颜色区分:待刻灰色、打坯蓝色、细雕橙色、完工绿色。颜色语义可以帮助师傅们扫一眼就知道哪些石头还没动。
下拉刷新用的是 Flutter 自带的RefreshIndicator,套在 ListView 外面即可。这里要留个心眼:RefreshIndicator 的 onRefresh 回调必须返回一个 Future,并且要等数据加载完才结束。如果回调里直接执行一个没有 await 的耗时方法,下拉动画会立即收回,用户根本不知道数据在加载。
详情页我用了CustomScrollView+SliverAppBar,实现“头部照片收起、内容上滑”的效果。照片放大查看用PhotoView插件,支持双指缩放,这对查看石料纹理细节是刚需。列表页到详情页的跳转加了一个 Hero 动画,让封面缩略图平滑放大成头部大图。这个动画成本极低,但体验提升非常直观,值得写进每一个图片型应用里。
4. 鸿蒙适配:从能跑通到真正可用
4.1 当前 Flutter 上鸿蒙的主流适配路线
Flutter 跑鸿蒙,并不是你装个官方 Flutter SDK 就能直接编译出 hap。目前主流路线是使用 OpenHarmony 社区维护的 Flutter 分支,这个分支在引擎层做了鸿蒙的适配,支持把 Dart 代码和引擎打包成鸿蒙可识别的产物。开发流程大致是:
- 安装 DevEco Studio,配置好 OpenHarmony SDK。
- 使用社区 Flutter 分支(支持 ohos 平台)。
- 在 Flutter 工程中启用 ohos 平台支持。
- 最后通过 DevEco 打开工程里的
ohos目录完成 hap 打包和签名。
整个过程里,Flutter 应用本身的代码几乎不需要为鸿蒙单独改写,UI 照常由 Flutter 引擎绘制。真正要动的地方是原生能力调用和权限声明,也就是 4.2 要讲的差异点。
有一点需要静下心认清:鸿蒙适配目前还在快速迭代,插件的原生实现不像 Android 生态那么齐全。选插件时第一反应应该查一下“是否支持 ohos”,不支持就用平台通道自己封装,别硬等某个插件适配。
4.2 迁移到鸿蒙后必改的 5 类差异
我梳理下实际移植时必须要处理的差异,做成表格放在最前面,后面详细交代每一项:
| 差异点 | Android 端 | 鸿蒙 ohos 端 | 建议做法 |
|---|---|---|---|
| 权限声明 | AndroidManifest.xml | module.json5 | 双端各维护一份权限列表 |
| 文件存储路径 | 内部存储/sdcard | 应用沙箱路径 | 一律用 path_provider 获取真实路径 |
| 平台通道 | Channel 名称自定义 | 同名 Channel,端侧实现不同 | 统一 Channel 命名,各端实现 |
| 图片选择/相机 | 有现成插件 | 插件缺失时应自封装 | 优先找 ohos 适配版插件 |
| 数据库 | sqflite 正常 | 用兼容 ohos 的分支 | 避免使用含原生扩展的桌面插件 |
权限声明这件事,Android 和鸿蒙各管各的,主工程里你要同时维护两份原生配置。刚开始我以为 Flutter 应用迁移到鸿蒙后连权限都不需要管,结果一调用相机直接崩,日志提示权限缺失,把相机权限写进 module.json5 之后才正常。
文件路径是另一个隐形坑。Android 上很常见的/storage/emulated/0/...这类绝对路径在鸿蒙的沙箱机制下根本不存在。凡是涉及文件读写,老老实实用path_provider(或者它支持 ohos 的等价实现)获取应用文档目录,不要手写路径。
还有一个不是很大但会让人懵的差异:布局体系。鸿蒙 ArkUI 里有一套自己的声明式语法,用 RelativeContainer、Flex、Tabs 这类组件。很多从鸿蒙原生转过来的开发者以为 Flutter 里也得用这些,其实不然——Flutter 应用内依然是 Row、Column、ListView 那套组件体系,ArkUI 的布局只在鸿蒙原生页面里才会用到。两者互不干扰,同一个应用可以一部分页面用 Flutter 写,一部分用 ArkUI 原生的 PlatformView 嵌入,怎么组合完全看你需求。
4.3 构建配置与 hap 打包
当 Flutter 工程需要鸿蒙产物时,流程和你熟悉的 Android 打包很相似:
flutter pub get flutter build hap --release这一步会在build/ohos下生成 hap 的中间产物。最终签名和发布,需要打开ohos目录工程,在 DevEco Studio 里配置签名证书后打包。
构建过程中有个高频问题:Flutter 引擎在鸿蒙侧打成一个类似 AAR 的模块,很多人会遇到“flutter aar 构建失败”的报错。这类问题十有八九是 SDK 版本不匹配或者缓存没清干净引起的。我的建议是:先跑flutter clean清掉所有缓存,再把 flutter SDK 切换到稳定版本,重新 build。如果还报错,把完整的构建日志贴出来,看具体卡在哪一步,不要东改西改。
签名方面,调试阶段 DevEco 会自动用调试证书,发布到市场或安装到多台设备则需要配置正式签名。这个和 Android 的 keystore 类似,提前在 DevEco 里配好,不要等到发版前一天才想起来。
4.4 用 PlatformView 和 MethodChannel 补上原生能力
Flutter 的插件生态再丰富,鸿蒙上的原生能力总有一两个没有现成实现。这时候平台通道就要出手了。
比如我需要调用一个设备震动反馈,提醒师傅“照片保存成功”,原生侧写一个极简的方法就够了:
static const MethodChannel _channel = MethodChannel('seal_archive/feedback'); Future<void> vibrate() async { try { await _channel.invokeMethod('vibrate'); } on PlatformException catch (e) { debugPrint('vibrate failed: ${e.message}'); } }鸿蒙原生侧在对应页面的生命周期里注册同样的 Channel 名称,实现 vibrate 方法,调用系统的震动接口。这里最容易犯的错是:Dart 侧 Channel 名称和原生侧不一致,或者原生侧没在正确的时机注册。Channel 名称是唯一的“通信协议”,两端名字对上号,方法名也对上号,数据就能传。
PlatformView 则用于嵌入原生控件。Flutter 里有些场景(比如复杂地图、特定扫描组件)用自绘实现成本过高,就适用原生 View 嵌入。鸿蒙的 PlatformView 能力和 Android 类似,但接口有差异,使用前一定先看对应适配分支的文档,别把 Android 的实现方式直接搬过去。
5. 高频报错与性能优化实录
5.1 构建与运行报错速查表
做跨端项目,报错十有八九不是写错代码,而是环境或配置问题。我把这阵子遇到的高频问题整理成一张表,照着排查能省很多时间:
| 现象 | 大概率原因 | 解决方法 |
|---|---|---|
| Flutter 新建项目后跑不起来 | Flutter SDK 版本老 / 设备未识别 | 跑flutter doctor,升级 SDK,重启模拟器 |
| Gradle 插件告警,提示不要用 apply script | 工程 build.gradle 用了旧式 apply | 按提示改用 plugins 块声明 |
| E/flutter ... dart_vm_initializer.cc(41): Unhandled Exception | 运行时异常未被捕获,常见为 null 检查崩溃 | 看完整 StackTrace,定位到具体行,处理空值逻辑 |
| flutter aar 构建失败 | 鸿蒙集成时引擎模块路径或缓存异常 | flutter clean后重新构建,检查 SDK 分支版本 |
| 平台通道 invokeMethod 返回异常 | Channel 名称不一致或原生侧未注册 | 核对两端 Channel 名称和方法名 |
| 依赖包版本冲突 | pubspec.lock 中间版本存在兼容问题 | 固定插件版本号,尽量用稳定版 |
E/flutter ... Unhandled Exception应该是出现频率最高、也最让人头疼的。这个报错没有任何业务信息提示,必须看下面的堆栈。我做项目时有次列表页下拉刷新闪退,堆栈指向了一个图片文件的 File 构造,原因是图片被手动删除后文件路径没更新,数据库里还是旧路径,一刷新就崩。周末排查了一下午,教训就是:凡是涉及外部文件的路径字段,读写时要主动判断文件是否存在,文件缺失就给默认占位图。
5.2 Future.then 与微任务:别再凭感觉写异步
写 Dart 异步代码最容易被问到的就是“Future 的 then 回调是不是放到微任务队列”。答案是:then 回调会被调度到微任务队列执行,但它的前置条件是要等到上一个 Future 完成。稍微展开一下。
Dart 事件循环有两个队列:事件队列(Event Queue)和微任务队列(Microtask Queue)。事件队列处理外部事件,微任务队列优先级更高,每次事件队列的顶部任务执行完毕后,会先把微任务全部清空再处理下一个事件。Future 的回调(then、catchError、finally 里的代码)逻辑上会被放进微任务队列,但不是同步立即执行,而是在当前事件处理完后的微任务阶段。
有一个很容易搞混的点:Future构造器里的代码,和Future后紧跟的.then里的代码,执行时机完全不同。
void main() { print('1'); Future(() => print('2')); // 这个异步任务进入事件队列(定时优先级为0) Future.microtask(() => print('3')); // 微任务 print('4'); } // 实际输出顺序:1、4、3、2为什么 3 比 2 先输出?因为Future.microtask直接把任务放进微任务队列,而Future()默认构造器注册的是事件队列任务(相当于 Timer(Duration.zero))。事件循环处理完当前同步代码后,第一件事就是清空微任务,所以 3 先走,事件队列里的 2 后面才执行。
这对前端界面意味着什么?耗时操作不要全丢到 then 链条里,尽量在独立 Isolate 做重活,把结果通过 SendPort 传回来。否则即使不卡主线程,微任务队列也会被长任务占满,界面响应依然会变迟缓。我优化过石料图片批量导入的逻辑,把图片压缩放到 compute 里跑,界面终于不再转圈。
5.3 图片列表内存与滚动性能优化
石料档案数量超过 300 条之后,列表页如果每张卡片都加载原图,内存一定爆。我分两步优化:
第一步是控制解码尺寸。Image.file显示缩略图时指定cacheWidth参数,强制 Flutter 按指定宽度解码,这样内存里只保留小图,而不是先解码出一张 4000x3000 的图再缩到 100x100。
Image.file( File(stone.coverPhotoPath), cacheWidth: 200, fit: BoxFit.cover, )第二步是生成独立的缩略图文件。拍照压缩时同时存一个_thumb后缀的小图,列表页读小图,详情页读大图。这样列表滚动时 IO 开销也小很多。实测 500 张照片的列表,滚动维持在 60 帧不掉帧。
另外一个容易忽略的优化是RepaintBoundary。列表项里如果有动画、圆角裁剪、阴影,Flutter 会在每次重绘时触发整卡片重绘。用RepaintBoundary包住每个卡片,隔离这部分绘制边界,滚动性能会有肉眼可见的提升。不需要额外的库,一个组件的事,值得每个列表都加上。
5.4 多终端版本兼容:老设备与新 API 共存
篆刻工作室的用户设备分布很杂,有的是最新旗舰机,有的是多年前的老款。Flutter 应用面对这种情况要在工程层面做几个约束:
- minSdkVersion 别设太高。太高会把老设备直接挡在门外,但设太低又可能碰到部分 API 不可用,权衡下来我习惯设在 24 左右。
- 插件版本锁定。pubspec.lock 里的插件版本建议保留锁定状态,不要频繁升级插件。跨平台适配中的插件升级很可能引入不兼容变更,为了一个新特性全盘升级老插件,不值得。
- 新 API 做运行时探测。某些能力我只在较新系统上启用,用
Platform.isAndroid加系统版本判断,在旧设备上走降级路径(比如不显示实时模糊效果、不加高级动画)。
老设备上最常遇到的不是系统 API 缺失,而是可用内存偏低。图片压缩、缩略图策略、禁用不必要的动画,本质上都是在帮老设备减轻负担。这个思路应该从第一天写代码就贯穿进去,而不是等用户反馈卡顿再补救。
鸿蒙适配走过一遍之后,我再回头看这个项目,最大的感悟是:跨平台开发最值钱的资产不是“一套代码跑三个端”这种结果,而是你在选型时愿意为“跑得稳”做的那些基础决策。数据模型先想清楚、状态别过度设计、图片从一开始就做压缩、路径永远用系统 API 获取、每个平台通道命名统一——这些看似琐碎的约束,最后都换成了“不需要反复修bug”的安静日子。
再分享一个压箱底的小技巧:给应用加一个“一键导出数据库”的按钮,导出的 SQLite 文件名带上时间戳。这个功能花不了 20 行代码,但换手机、给别的师傅传数据时,你会感谢当初写这个按钮的自己。档案类应用最怕的不是功能少,而是数据没了。