
家里买了几件大件家具之后我就一直想弄个App把价格、购买日期、售后信息都记下来。用表格记不方便拍照存相册又容易丢。正好最近在折腾Flutter又刚好拿到了OpenHarmony的开发板干脆把两者结合起来做一个跨端的家具购买记录App。这篇文章就聚焦在其中一个我觉得最有代表性的模块「添加房间」的实现。别小看这个功能它牵扯到数据建模、本地持久化、UI交互、状态管理还有Flutter和OpenHarmony原生能力的协调整合几乎是整个Flutter跨端开发流程的一个缩影。这篇文章适合真正想上手Flutter OpenHarmony开发的人尤其是已经在OpenHarmony设备上跑过Demo、想知道怎么把业务功能落地的人。我会把整个功能从设计到编码的过程原原本本捋一遍重点讲清楚每一步「为什么这么选」而不是单纯贴代码。看完之后你可以直接把这套思路搬到自己的项目里。1. 项目背景与整体架构设计1.1 家具购买记录App的核心需求拆解家具购买记录这个场景听起来简单实操细节其实不少。我最初的需求就三条记录每件家具的关键信息名称、品牌、价格、购买日期、保修期限给家具分空间管理客厅、卧室、厨房以及支持拍照留底发票、安装说明、实物照片。「添加房间」就是整个App的地基。没有房间家具就没地方挂靠房间的增删改查如果做得不顺手后续所有家具录入都会被拖累。更关键的是这个功能天然带有「列表展示 表单录入 数据持久化 页面状态同步」四个维度正好把Flutter开发的核心知识点串起来了。房间的字段在设计时就刻意往实用方向靠不只是存一个名字那么简单字段类型说明roomIdString主键用UUID生成nameString房间名称如「客厅」iconString房间图标标识映射到Material IconssortIndexint手动排序权重默认按创建时间递增createdTimeint创建时间戳毫秒itemCountint房间内家具数量列表页直接展示其中itemCount是冗余字段每次操作完家具要同步更新它。当时有人问我为什么不直接join查家具表我的考虑是这个App的数据量级就是几百条级别join没有任何性能优势反而让房间列表的加载逻辑变得复杂。用冗余字段房间列表一次性读出来就能显示数量避免了多次异步回调。1.2 为什么在OpenHarmony上选Flutter而不是ArkTS很多人在社区里讨论arkts和flutter谁更流行其实这个问题的答案完全取决于你的业务目标。如果你只做OpenHarmony一个平台ArkTS当然是首选毕竟它是系统的原生语言API调用最直接工具链也最匹配。但如果你想一套代码同时覆盖Android、iOS和OpenHarmonyFlutter的优势就非常明显了。我这边的实际情况是App要先在手机上用起来后续再平滑迁移到OpenHarmony的带屏设备比如开发板、智能家居中控屏。选择Flutter核心原因是它的自绘引擎保证了UI在所有平台上表现完全一致。你不需要在Android调一遍约束布局、在OpenHarmony再调一遍ArkUI的布局参数只要写好一套Widget三个平台一个样。另外一个很现实的因素是热重载。Flutter在开发调试时的热重载体验在同类的跨端方案里是数一数二的。改完房间列表的卡片样式保存后一秒钟就能看到真实效果这对我这种需要频繁调UI的人来说太关键了。在ArkTS开发中虽然也有预览器和热重载但整体调试体验还在逐步完善中。不过也别把Flutter在OpenHarmony上的支持想得过于轻松。目前OpenHarmony的Flutter SDK和引擎是由社区团队维护的整体进度落后于上游Flutter版本部分原生插件需要自己适配。所以我的原则是业务逻辑尽量用纯Dart实现少依赖平台专属插件把原生调用收敛到几个明确的边界上。这样即便底层SDK升级我只需要小范围调整接口适配层。1.3 项目目录结构与模块划分为了让「添加房间」这个功能不变成一坨散代码我参照成熟Flutter项目的分层思路把目录结构划分得很清晰lib/ |-- models/ # 数据模型定义Room、FurnitureItem |-- data/ # 数据访问层数据库Helper、DAO、仓库Repository |-- providers/ # 状态管理RoomProvider、FurnitureProvider |-- pages/ | |-- home/ # 首页与房间列表 | |-- room/ # 房间新增/编辑页面 | -- furniture/ # 家具列表与家具编辑页面 |-- platform/ # 平台通道封装相机调用、相册读取 |-- utils/ # 通用工具时间格式化、ID生成器 -- widgets/ # 通用组件空状态组件、确认弹窗这么分的好处是页面代码只关心UI渲染和用户交互不直接操作数据库Provider只负责状态流转和通知刷新不关心数据从哪来DAO层把SQL语句收敛到一个文件里将来换存储方案比如从SQLite换到对象关系映射框架时改动面被限制在数据层内部。我自己在多个项目里试过「页面直连数据库」的写法前两周很爽后面功能一多就乱了所以这次一开始就遵守分层约束。2. 环境准备与工程搭建能踩的坑我帮你先踩了2.1 OpenHarmony上的Flutter开发前置条件想在这个平台上跑Flutter首先得把SDK和引擎装对。OpenHarmony的Flutter支持目前并不是通过官方flutter SDK直接内置的而是由OpenHarmony SIG团队维护了一个独立的SDK分支。具体来说你需要准备四样东西OpenHarmony SDK通过DevEco Studio安装或直接下载命令行工具包OpenHarmony的Flutter SDK分支需要克隆特定仓库并切换分支Flutter引擎的OpenHarmony预编译产物DevEco Studio用于编译OpenHarmony原生工程查看原生日志我这次用的是OpenHarmony 5.0和对应的Flutter SDK master分支。这里要提醒一句分支和版本之间的匹配非常关键。如果你先把Flutter SDK切到了最新而引擎产物还是旧的编译时会报一堆奇怪的符号缺失错误排查起来极其痛苦。建议严格按照仓库说明里的版本对照表安装不要自己乱配。2.2 从零创建一个支持OpenHarmony的Flutter工程创建工程的流程和普通Flutter项目不太一样不能只用flutter create就指望OpenHarmony能跑。正确做法是先创建标准Flutter工程然后通过DevEco Studio把HarmonyOS原生外壳加进来。我操作的步骤是这样先执行flutter create furniture_app生成纯Flutter工程。打开DevEco Studio选择Import导入刚才的flutter工程。DevEco会自动识别Flutter模块并要求配置OpenHarmony SDK路径。配置完成后在DevEco里选择entry模块作为启动入口等待Gradle相关任务执行完成。第一次运行到OpenHarmony设备时确保设备开了开发者模式和USB调试。实际过程并没有这么顺利。我最开始直接用DevEco新建了一个ArkTS工程然后试图把Flutter模块塞进去结果折腾了一下午各种依赖对不上。后来改用「Flutter工程为主DevEco导入」的方式几分钟就跑通了。如果你的目标只是让现有Flutter项目适配OpenHarmony强烈建议走这个路线不要反向操作。2.3 flutter新建项目后跑不起来大概率是环境问题网上搜flutter相关资料时「新建项目后跑不起来」几乎是人人都遇到过的坎。在OpenHarmony上这个问题出现的概率更高而且错误信息往往更隐晦。我遇到的典型报错有这么几类第一类是SDK版本找不到运行flutter doctor时显示OpenHarmony SDK not found。解决办法是配置本地环境变量指向DevEco安装目录下的OpenHarmony SDK路径同时确保local.properties文件里的sdk.dir写对了。第二类是引擎初始化失败运行时日志里出现Failed to load flutter engine相关的字样。这多半是因为预编译引擎版本和Flutter SDK版本不匹配。我建议把OpenHarmony SIG提供的「Engine SDK」组合包整个下载下来不要分开配。第三类是ide侧同步问题DevEco导入工程后一直卡在gradle依赖下载。这种其实没什么技巧就是等偶尔需要清楚Gradle缓存重新同步。如果公司网络拉取依赖特别慢可以配镜像仓库但注意不要用一些来路不明的第三方源尽量用公认的企业级镜像。3. 添加房间功能的数据层设计先把地基打牢3.1 房间数据模型设计与字段取舍数据模型是整个功能稳定的基础。我上面已经列过Room表的字段设计这里着重展开两个决策点。第一个是roomId的生成策略。刚开始我想当然地用了自增整数ID后来发现多设备同步场景下手机和开发板做数据互通自增ID几乎必然冲突。所以我改成了Dart侧用Uuid包生成随机字符串数据库里作为主键后续如果上云同步也能直接当全局标识符用。第二个是icon字段的处理。房间图标我用了Material Icons里的预设集合比如客厅用weekend、卧室用bed、厨房用kitchen、书房用menu_book、卫生间用bathtub。存数据库的时候不要存图标组件对象本身只存一个字符串标识UI层做映射。这样将来想换图标库只需要改映射层不用动数据库。还有createdTime字段我特意存了毫秒级时间戳而不是SQLite的日期字符串。时间戳做排序、比较、格式化都很方便而且不受数据库时区设置影响。显示的时候再通过Dart的DateTime转成本地时间字符串。3.2 本地持久化选型SQLite还是shared_preferences做本地数据存储Flutter生态里最常用的三个方案是shared_preferences、sqflite、Hive。在OpenHarmony平台上这三个方案的适配情况差异很大这里需要认真对比一下。shared_preferences在OpenHarmony上虽然有社区适配版本但只适合存键值对。房间列表这种结构化数据用shared_preferences就非常别扭每次增删改都得把整个列表序列化成JSON再写回还得自己做并发控制。数据量小的时候能用但代码不好维护。Hive是一个纯Dart实现的轻量级NoSQL数据库理论上跨平台能力很强。但我在OpenHarmony上实测时发现某些版本依赖原生文件IO的路径策略和OpenHarmony沙箱机制有轻微冲突需要额外适配。如果不想折腾不太建议在OpenHarmony项目里首选Hive。我最终选的是sqflite的OpenHarmony适配版本。SQLite本身是C库OpenHarmony系统里自带了对SQLite的支持所以性能和数据可靠性都有保障。SQL的写法也让数据管理思路更清晰——房间表和家具表之间的关联关系用SQL表达远比其他方案直观。3.3 数据库Helper与DAO层封装细节我的数据库版本号定为1只包含两张表room和furniture_item。建表语句里用到了外键约束和索引final String createRoomTable CREATE TABLE room( room_id TEXT PRIMARY KEY, name TEXT NOT NULL, icon TEXT DEFAULT home, sort_index INTEGER DEFAULT 0, item_count INTEGER DEFAULT 0, created_time INTEGER NOT NULL ) ; final String createFurnitureTable CREATE TABLE furniture_item( item_id TEXT PRIMARY KEY, room_id TEXT NOT NULL, name TEXT NOT NULL, brand TEXT, price REAL, purchase_date TEXT, warranty_years INTEGER, photo_path TEXT, remark TEXT, created_time INTEGER NOT NULL, FOREIGN KEY(room_id) REFERENCES room(room_id) ON DELETE CASCADE ) ; final String createIndexOnFurniture CREATE INDEX idx_furniture_room ON furniture_item(room_id) ;外键ON DELETE CASCADE的意义很直接删除房间时该房间下所有家具条目自动删除应用层不用写循环删除逻辑。数据库索引则保证了按房间查询家具时的性能虽然数据量小的时候感受不明显但这是正确习惯。DAO层我只暴露了有限几个方法queryAllRooms、insertRoom、updateRoom、deleteRoom、updateRoomItemCount。方法名直接对应业务动作不写通用的泛型查询方法。实际经验告诉我过度设计的DAO层往往会变成「抽象泄漏」反而不如这种一眼看懂的方法列表好用。3.4 多房间联动和数量同步的实现思路添加房间本身不涉及家具数量但我顺手把「房间内家具计数」的逻辑一并讲了。这里用了一个很笨但很有效的办法在Repository层定义一个统一的方法addFurnitureToRoom内部事务性地执行「插入家具记录、递增房间计数」两步操作。Futurevoid addFurnitureToRoom( Database db, FurnitureItem item, String roomId, ) async { await db.transaction((txn) async { await txn.insert(furniture_item, item.toMap()); await txn.rawUpdate( UPDATE room SET item_count item_count 1 WHERE room_id ?, [roomId], ); }); }为什么用事务因为如果先插入家具成功、再更新计数失败数据就永久不一致了页面显示的「房间内X件家具」就会少一件。事务保证这两个操作要么同时成功、要么同时回滚。这个设计虽然简单但如果你是新手很值得养成习惯。房间排序方面我用sortIndex加createdTime双字段。默认情况下新房间的sortIndex等于当前时间戳排在列表最后用户长按拖动排序时只更新sortIndex字段页面按sortIndex升序排列。这样避免了复杂的位置交换逻辑同时保持了稳定的排序体验。4. 添加房间的UI与交互实现从列表到表单4.1 房间列表页面第一印象决定了用户是否愿意继续用房间列表是整个App的入口界面我采用了卡片流式布局而不是传统的列表行。每张卡片显示房间图标、名称、家具数量、创建时间以及一个右上角的菜单按钮编辑、删除。卡片底部预留了一个「添加房间」的占位卡片点击直接进入新增页面。这里有个交互细节值得分享占位卡片的设计比悬浮按钮更符合直觉。用户看到一排房间卡片自然会在最后看到一张虚线边框的「 添加房间」卡片这种发现式的引导比右下角一个悬浮按钮的转化率高很多。我自己测试的体验也证实了这一点所以没有使用FloatingActionButton。空状态我也处理了一下。首次打开App时没有房间数据页面中间显示一个带图标和说明的空状态组件并配一个「添加第一个房间」按钮。这个组件做成了通用widget后续家具列表页面空状态也能复用。空状态的文案不要写「暂无数据」这种冷冰冰的话写成「还没有房间先建一个吧」会更有引导性。4.2 添加房间表单校验、弹窗与页面返回的完整流程添加房间页面就是典型的表单页。核心表单字段只有两个房间名和房间图标但UI上我加了第三个「建议命名」的辅助文本比如点击「客厅」快捷输入按钮直接填入房间名并选中对应图标。表单校验逻辑很关键。房间命名为空时在输入框下方显示「房间名不能为空」名字和现有房间重名时提示「已有同名房间」并且清空输入并获取焦点。重名校验要放在提交时而不是实时监听避免用户还在输入中文拼音时就被判定为重复。我复盘过这个重名逻辑的一个坑中文输入法在Flutter的TextField里会有合成文本阶段onChanged会多次触发如果实时查重用户还没选完拼音就被拦截了。所以我的方案是只在_submit()方法里查重这也是正确处理用户输入类校验的通用原则。提交成功后页面通过Navigator.pop(context, true)返回列表页并用返回值告诉列表页「数据已变化请刷新」。这种通过路由返回值做页面通信的方式比用全局变量通知要干净得多不会留下诡异的状态残留。4.3 Icon选择器与表单控件的交互实现图标选择器我实现为一个横向滚动的选择列表下面放一排Material Icons图标用户点选后高亮显示。这不是最复杂的交互但有个细节值得提图标条目在选中时需要有明确的视觉反馈缩放动画加边框颜色变化让用户感受到「选中了」。用到的图标集合是固定的我写在了一个常量文件里const roomIcons [ Icons.weekend, // 客厅 Icons.bed, // 卧室 Icons.kitchen, // 厨房 Icons.menu_book, // 书房 Icons.bathtub, // 卫生间 Icons.checkroom, // 衣帽间 Icons.deck, // 阳台 Icons.child_care, // 儿童房 ];图标选择器本身不存文件只是把字符串标识交给上层比如客厅就存weekend。渲染时再通过一个Map把字符串映射回IconData。这种约定在Flutter里很常用因为IconData不能直接序列化但字符串可以随意存。4.4 拍照记录功能Flutter侧调用OpenHarmony相机家具购买记录App不能没有拍照功能。购买单据、家具实物、安装说明都建议拍照归档。在OpenHarmony上Flutter侧无法直接用image_picker插件这个插件的官方实现目前没有覆盖OHOS平台所以需要走平台通道调用OpenHarmony原生相机能力。我在Flutter侧封装了一个CameraHelperclass CameraHelper { static const MethodChannel _channel MethodChannel( com.example.furniture_app/camera, ); static FutureString? takePhoto() async { try { final String? path await _channel.invokeMethod(takePhoto); return path; } on PlatformException catch (e) { debugPrint(拍照失败: ${e.message}); return null; } } }这里有一个典型的异步边界调用invokeMethod返回的文件路径是String实际图片已经由原生侧保存到了应用缓存目录。Flutter侧拿到路径后用Image.file读取展示。为什么不让原生直接返回图片字节因为大图传字节数组在通道里会触发内存峰值问题顺带说一句Flutter和原生之间的二进制传输并不是免费的传一张几MB的照片很容易卡顿。5. 状态管理与组件通信让页面数据流动起来5.1 Provider状态管理三步法ChangeNotifier到ConsumerFlutter的状态管理方案五花八门但我在这个小项目里选了Provider。原因很简单Provider的学习曲线低、官方文档清晰、性能足够应对这种中小型App。如果你纠结flutter provider怎么用其实核心就三步。第一步定义状态类继承ChangeNotifier。class RoomProvider extends ChangeNotifier { ListRoom _rooms []; bool _isLoading false; ListRoom get rooms _rooms; bool get isLoading _isLoading; Futurevoid loadRooms() async { _isLoading true; notifyListeners(); _rooms await RoomRepository.instance.queryAllRooms(); _isLoading false; notifyListeners(); } Futurevoid addRoom(Room room) async { await RoomRepository.instance.insertRoom(room); await loadRooms(); } }第二步在App顶层用MultiProvider注入多个Provider实例。runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (_) RoomProvider()), ChangeNotifierProvider(create: (_) FurnitureProvider()), ], child: const FurnitureApp(), ), );第三步在界面中通过context.watch或Consumer订阅状态变化重建UI。房间列表页的ConsumerRoomProvider会监听rooms变化一旦Provider调用了notifyListeners列表页自动刷新。这套流程理解起来很顺ChangeNotifier就是「数据变化的广播器」Provider负责把广播器和UI绑定Consumer/context.watch是UI端的订阅者。用这个思路你不需要背各种API只需要知道「改完数据就notifyListeners」。5.2 什么时候不该用Provider我踩过的性能坑Provider虽好但也不是万能的。我在实现房间列表的拖动排序时踩过一个明显的性能坑每个房间卡片都context.watchRoomProvider()拖动过程中每个卡片都会反复重建导致掉帧。原因就是拖动时的每一次位置变化都触发了notifyListeners整个列表所有卡片全部重建浪费极大。解决办法是把「可变的排序状态」和「不可变的房间数据」分开管理。房间列表本身不放在Provider里而是用一个单独的ReorderableListViewStatefulWidget管理本地状态只有增删改时才通知Provider刷新数据。这种「UI交互状态用本地State共享业务数据用Provider」的混合模式是我现在做Flutter布局的首选策略。所以状态管理不是「越全局越好」。全局状态越多刷新范围越大性能隐患越明显。在动手写Provider之前先想清楚这个状态到底有几个页面要共享只有真正需要跨页面同步的状态才放进Provider。5.3 Flutter组件通信方式全景对比关于flutter组件通信很多人一开始搞不清楚什么时候用回调、什么时候用Provider、什么时候用EventBus。我在这个项目里做了一个简单对照通信场景推荐方式说明子组件通知父组件回调函数Callback最简单直观适合局部交互父组件控制子组件GlobalKey用于调用子组件的公开方法跨页面共享业务数据Provider/ChangeNotifier适合全局数据流Flutter与OpenHarmony原生MethodChannel/EventChannel跨运行时通信页面间传参构造函数参数/Navigator路由参数简单且安全回调是最基础的手段。比如房间卡片右上角的菜单按钮通过onEdit和onDelete回调把事件抛给父页面处理。这种模式在组件复用场景下极其常见不要一上来就用Provider回调能解决的问题用回调就够了。EventChannel则是一个反向通道适合原生侧主动向Flutter推送事件比如相机权限状态变化、系统相册选中回调等。和MethodChannel的「请求-响应」模型不同EventChannel是持续的流。我在后面摄像头接入的部分会再展开。5.4 Provider在添加房间全流程中的数据流走向把整个添加房间的数据流理顺你会发现状态管理的价值就体现在这里。完整流程是这样的用户点击「添加房间」按钮。列表页通过Navigator.push打开RoomEditPage不传任何数据新增模式。用户填写表单并点击保存。表单页构造Room对象调用context.readRoomProvider().addRoom(room)。RoomProvider执行Repository层写入数据库。写入成功后Provider的loadRooms()重新查询数据库。查询结果赋值给_rooms调用notifyListeners。列表页的Consumer收到通知自动用新列表重建UI。表单页Navigator.pop(true)关闭。列表页同时显示新房间卡片数据同步完成。这个流程里关键的设计是Provider内部永远从数据库拉最新数据而不是把表单页构造的Room对象直接塞进内存列表。因为如果你只塞内存不写库下次冷启动房间就消失了会制造出「假成功」的错觉。养成从Repository层读数据的习惯能避免非常多隐蔽的bug。6. 平台通道架起Flutter和OpenHarmony之间的桥6.1 为什么需要MethodChannel自定义原生代码Flutter本身不能直接操作OpenHarmony的相机、相册、通知这些系统能力。虽然Dart层能通过一些通用插件间接实现但OpenHarmony的插件生态远不如Android成熟。所以当业务需要原生能力时必须自己写原生代码桥接。MethodChannel就是Flutter和原生侧之间的一条双向通信管道。它提供了一套标准化的消息编解码机制Dart侧传入方法名和参数原生侧注册处理方法处理后把结果返回给Dart侧。整个过程是异步的不会阻塞UI线程。我这次接相机功能就是走MethodChannel。Dart侧调用invokeMethod(takePhoto)OpenHarmony原生侧启动相机界面拍完把照片保存到沙箱路径最后通过result.success(path)把路径传回Flutter。整个设计非常清晰。6.2 OpenHarmony原生侧如何响应Flutter调用的方法OpenHarmony侧接收Flutter的调用是在ArkTS代码里通过配置好的Engine和MethodChannel完成的。核心思路是把UIContext或者对应的Ability上下文绑定到Flutter引擎然后注册方法处理器。这段逻辑的伪代码结构如下关键部分是setMethodCallHandlerlet storage new LocalStorage(); let ctx storage.getContext() as common.UIAbilityContext; let flutterController new FlutterController(ctx); flutterController.loadContent(...); let flutterEngine flutterController.getEngine(); let channel new MethodChannel( flutterEngine, com.example.furniture_app/camera ); channel.setMethodCallHandler((call) { if (call.method takePhoto) { // 调用相机Picker // 获取照片路径 call.result.success(photoPath); } });这里有几个初学者容易踩的点。第一MethodChannel的channelName必须和Dart侧完全一致少个点都会导致「找不到处理器」。第二处理耗时的调用时比如相机拍照在setMethodCallHandler里使用异步闭包先返回result占位再在拍摄完成后补调result.success。第三原生侧必须在UIAbility的onCreate生命周期里完成channel注册如果注册晚了早期调用会直接超时失败。6.3 EventChannel反向推送原生主动通知Flutter场景除了Flutter主动调用原生有时候原生侧需要主动向Flutter发消息。典型场景是用户在系统相机界面里取消了拍照或者拍完照片后原生侧做了图片压缩压缩完成的时间点需要通知Flutter更新界面。EventChannel的职责就是承载这种反向数据流。Flutter侧先从EventChannel建立订阅关系然后原生侧在合适的时机向channel发送事件Flutter侧接收并处理。用法如下EventChannel _eventChannel const EventChannel( com.example.furniture_app/camera_events, ); Streamdynamic _onNativeEvent() { return _eventChannel.receiveBroadcastStream(); } // 在页面初始化时订阅 _streamSubscription _onNativeEvent().listen((event) { if (event photoSaved) { setState(() { _photoSaved true; }); } });很多人会忽略EventChannel的取消订阅导致页面销毁后内存泄漏这一点值得特别注意。在dispose方法里必须调用_streamSubscription.cancel()。我在真机调试时就因为这个遗漏导致每次进入页面内存占用上浮排查了半天才定位。6.4 平台通道适配OpenHarmony时的兼容性建议不同设备上的OpenHarmony版本差异很大开发板、平板、智能屏的系统能力不尽相同。平台通道的代码尽量做防御式编写。方法和参数的命名要统一不要在不同设备上使用不同的通道名。OpenHarmony上部分旧版本设备对相机权限的处理也和手机不完全一样需要在原生侧把权限校验写好前置判断下。另外一个兼容性策略把平台通道封装在接口后面业务代码依赖接口而非直接依赖MethodChannel。这样如果某个设备不支持相机功能我可以在接口实现里返回nullUI层展示「该设备不支持拍照」的提示而不是直接崩溃。跨平台开发的一个铁律就是永远假设某个能力在你的目标平台上是缺失的然后把它作为正常分支处理。写到这里「添加房间」这个功能从设计到实现的主干已经全部走通了。我自己在实际操作中最深的一个体会是FlutterOpenHarmony的坑其实要比想象中少得多只要环境版本匹配好、原生通道命名一致、数据层和状态层边界清晰整个开发节奏是非常顺畅的。后续这个项目我计划继续把家具编辑、价格统计、保修提醒加进来方向已经清楚了。如果你正在考虑把自己的Flutter项目跑上OpenHarmony我的建议就是拿一个像「添加房间」这样的小功能练手把数据、UI、原生通道整个链路打通一次后面再扩功能就水到渠成了。