☰
Flutter鸿蒙适配实战:消息反馈系统的跨端实现与踩坑
2026/9/30 8:11:53 网站建设 项目流程

先把结论放在前面:一个 Flutter 项目要上一个新平台,最麻烦的从来不是把页面跑起来,而是那些要跟系统原生能力打交道的模块。消息反馈就是这样一类典型模块——表面上不过是一个“表单加列表”,真正落地的时候,通知、角标、推送、图片选择、状态同步全都要有原生侧配合。这次“享家社区”项目,就是把整套消息反馈系统用 Flutter 实现,再完整适配到 HarmonyOS 上跑通。

“享家社区”本身是一个面向小区场景的社区服务 APP,业主在里面报事报修、投诉建议、咨询求助,物业和客服在后面接单处理。消息反馈模块要支撑的不是一个“提交成功”的提示,而是一条完整闭环:业主提交问题 → 系统分单 → 物业受理 → 处理中 → 完成确认 → 评价。过去这些反馈散落在电话、微信、物业群里,根本没法追溯;做成 APP 里的消息反馈系统之后,每条记录都有状态、有处理人、有处理时长,管理侧还能盯催超时工单。这篇文章我会把整体设计思路、Flutter 与鸿蒙之间事件通道的打通、反馈中心的交互与状态管理,以及鸿蒙适配阶段踩过的那些具体坑完整写出来。适合正在做 Flutter 跨端、又想同步覆盖鸿蒙的团队,也适合还没想清楚“消息反馈这类模块到底该怎么设计”的人。

1. 这届消息反馈系统,到底差在哪:整体设计与技术选型

1.1 消息反馈不只是“填个表单”:闭环与状态机

在享家社区这个场景里,反馈内容的真实类型其实很杂:报事报修(水电、电梯、门禁、公区卫生)、服务投诉(保洁、保安、管家)、邻里建议、咨询求助,甚至还有表扬。不同类别要落到不同的处理人,所以第一个设计决定不是表单字段怎么排,而是分类与路由。如果一开始只做“用户输入 + 提交成功”,后面大概率要重做,因为运营侧根本不知道谁来接单。

我们把整个流程定义成这么一条链路:

  1. 业主提交反馈,系统自动按“小区 + 楼栋 + 分类”打标签;
  2. 客服或物业值班人员受理,把状态从待受理改成处理中;
  3. 处理人填写处理结果,状态变成已完成待确认;
  4. 业主侧看到处理结果,确认没问题后关闭,并做满意度评价;
  5. 如果业主对处理结果不满意,可以一键追问,状态回到处理中。

这里最关键的是状态机。我们定义了五种状态:pending(待受理)、processing(处理中)、resolved(已完成待确认)、closed(已关闭)、reOpened(用户追问后重新打开)。状态流转不能乱跳,比如 resolved 之后不可以直接回到 pending,只能通过业主追问变成 reOpened,再进入 processing。这个规则在代码层面用枚举 + 合法转移表做死了,避免后台上有人手动乱改状态导致用户侧看到错乱进度。

超时规则也是消息反馈系统里比较容易被低估的部分。我们定的业务参数是:受理后 24 小时仍未处理,自动生成催办记录;48 小时仍未处理,工单在处理人列表里置顶并标红;超过 72 小时未响应,自动升级到值班长介入。催办的实现不复杂,就是后台定时任务扫一遍 updatedAt,但前端在展示的时候需要区分“普通反馈”和“已超时反馈”,这两种卡片在列表里的视觉权重是不一样的。

1.2 为什么选 Flutter 来做 HarmonyOS 端:成本、复用与限制

选 Flutter 这件事,团队内部其实是讨论过的。鸿蒙原生开发有自己的 UI 框架和工具链,如果只做鸿蒙单端,直接用原生做当然最稳。但享家社区的用户同时分布在 Android、iOS 和鸿蒙设备上,反馈模块的逻辑又高度一致,三端各写一套的成本实在不划算,所以最后决定用 Flutter 作为跨端方案。

这里有个背景要说清楚:Flutter 官方目前并没有直接给 HarmonyOS 发布稳定分支,鸿蒙端跑 Flutter 用的是基于 OpenHarmony 的适配 SDK(社区和厂商维护的 fork)。UI 层大部分代码是可以复用的,因为 Flutter 自带渲染引擎,不依赖系统控件;但凡是涉及系统能力的插件,比如推送、定位、相册选择、通知栏,都需要确认它在鸿蒙侧是否有原生实现。没有实现的话,就只有两条路:自己写桥接,或者换一个已经适配好的方案。

这个选型逻辑说白了就是:业务代码复用是目的,原生能力按需补齐是代价。反馈模块在整个 APP 里属于“中等偏重原生依赖”的模块,恰好适合用来验证 Flutter 跨端到鸿蒙的可行性。如果连它都能在鸿蒙上顺利跑通,那其他更纯粹的业务页面基本没有适配风险。

1.3 消息模型与状态流转的落地设计

消息反馈的数据模型不用设计得特别复杂,但字段不能漏。我们 Dart 侧的核心模型大致长这样:

enum FeedbackStatus { pending, processing, resolved, closed, reOpened } enum FeedbackCategory { repair, complaint, suggestion, consult, praise } class FeedbackMessage { final String id; final String userId; final String communityId; final String building; final FeedbackCategory category; final String title; final String content; final List<String> imageUrls; final FeedbackStatus status; final String handlerId; final DateTime createdAt; final DateTime updatedAt; final DateTime? handledAt; bool get isOverdue => _checkOverdue(); bool _checkOverdue() { if (status == FeedbackStatus.closed || status == FeedbackStatus.resolved) { return false; } return updatedAt.isBefore(DateTime.now().subtract(const Duration(hours: 48))); } }

注意 imageUrls 和 content 都需要做长度限制,图片我们压到单张不超过 1MB 再上传,内容限制在 500 字以内。这里有个经验:反馈文本不做限制会让后台审核非常痛苦,字数限制要放在 UI 层就拦住,而不是等提交到接口再报错。

存储上我们做了两层:第一层是服务端接口,负责持久化和分单;第二层是本地缓存,用 Hive 或者类似 key-value 方案保存用户未提交的草稿。理由很实际:业主在反馈输入框里写了 200 字,结果切后台被系统回收,回来内容全没了,这种体验在反馈场景里是致命的。我们的做法是输入内容每停顿 500ms 自动写入本地草稿,提交成功后再清掉。这个细节看着小,实测对反馈完整率提升非常明显。

2. Flutter 与鸿蒙原生之间的那扇门:事件通道与平台视图

2.1 哪些能力必须走原生桥:事件通道的定位

Flutter 和原生侧通信的方式其实就三兄弟:MethodChannel、EventChannel、PlatformView。很多人对它们的使用场景分不清楚,导致代码写得别别扭扭,这里用反馈模块的实例一次说清楚:

  • MethodChannel是“请求-响应”模式,Dart 侧调用,原生侧处理完返回结果。适合一次性操作,比如获取推送 token、申请权限、把图片原始路径交给原生侧做压缩处理。
  • EventChannel是“持续事件流”模式,原生侧主动往 Dart 侧推数据。适合推送到达、反馈状态变化、角标变化这类“不知道什么时候会发生”的事件。
  • PlatformView是把原生控件嵌进 Flutter 页面里。适合反馈提交页里的地图选点、富文本编辑器这类 Flutter 侧实现成本高的控件。

在消息反馈系统里,最容易踩的坑就是把 EventChannel 当成 MethodChannel 用:有人在 Dart 侧每次需要新数据的时候去调一个“获取最新反馈状态”的桥接方法,其实原生侧早就可以通过事件流推过来。事件驱动的直觉一旦建立起来,后面加新消息类型就是加一个枚举值的事。

2.2 EventChannel 接入实录:从推送到达页面红点

这里写一段真实接入过程。场景是:后台判定某条反馈“已解决”,鸿蒙侧收到推送服务下发的通知,APP 要把这个状态变化实时同步到反馈列表,并更新底部 Tab 的红点角标。Dart 侧实现如下:

class FeedbackEventBridge { static const EventChannel _eventChannel = EventChannel('community/feedback/events'); static const MethodChannel _methodChannel = MethodChannel('community/feedback/method'); Stream<Map<String, dynamic>> get onFeedbackEvent { return _eventChannel .receiveBroadcastStream() .map((event) => Map<String, dynamic>.from(event as Map)); } Future<String?> fetchPushToken() async { final token = await _methodChannel.invokeMethod<String>('getPushToken'); return token; } }

在 APP 启动后的全局位置订阅一次,不要在反馈列表页的 initState 里订阅,否则原生侧事件推过来的时候如果页面还没创建,事件就丢了。我们用的模式是:启动时订阅事件流,事件到达后由内存中的一个ValueNotifier<int>去驱动红点和列表状态,页面只负责监听这个 notifier。

鸿蒙原生侧的关键代码是往 EventChannel 的 sink 里塞数据。大致逻辑如下(具体类名以你当前使用的 Flutter 鸿蒙引擎版本为准):

let eventChannel = new FlutterEventChannel('community/feedback/events'); eventChannel.setStreamHandler({ onListen: (args, sink) => { this.eventSink = sink; }, onCancel: (args) => { this.eventSink = null; }, }); // 收到“反馈已解决”的推送后 this.eventSink?.success({ type: 'statusChanged', feedbackId: '1001', status: 'resolved', });

一个特别容易忽略的坑是启动时序。推送可能发生在 Dart 侧还没监听的时候,所以原生侧收到推送后不能直接丢给 sink,而要在内存里缓存最近一条事件。当 Dart 侧 onListen 触发时,先把缓存的事件补发一次,之后再实时推。不加这一步,用户从冷启动进 APP 后经常看不到最新一条状态变化,还以为是接口同步慢。

2.3 MethodChannel 与 PlatformView:发反馈与原生控件的混合使用

再讲 MethodChannel 的实操。反馈提交页允许业主选图上报,但 Flutter 社区常见的图片选择插件不一定有鸿蒙实现,我们最后直接走了 MethodChannel 自建桥接:Dart 侧把需要调用的动作和参数传给原生,原生侧负责调系统相册、申请相册权限、拿到图片原始路径后做压缩,最后把压缩后的临时文件路径返回给 Dart 侧。

final List<String> compressedPaths = await _methodChannel.invokeMethod( 'pickAndCompressImages', {'maxCount': 6, 'maxSizeKb': 1024}, );

这里有一个原则:大文件处理不要想着用 MethodChannel 传二进制流。图片原图动不动几 MB,直接以字节数组方式在 Dart 和原生之间搬运,通道会卡死,甚至触发 OOM。正确做法是原生侧处理完文件,把结果写到临时目录,通道里只传路径字符串。

PlatformView 的场景我们用在“定位楼栋”上。提交反馈时要让业主选择具体楼栋,这个选择器嵌的是鸿蒙侧地图选择控件,通过 PlatformView 塞进 Flutter 页面。实际体验下来,PlatformView 最大的问题是手势冲突和键盘弹起时的重绘,鸿蒙侧尤其要留意地图控件被缩放时是否会白屏。我们的规避办法是:在 PlatformView 外层套一个固定尺寸的容器,禁止 Flutter 侧对它做缩放变换,需要用地图全屏时直接打开原生全屏页面,而不是把 PlatformView 放大。

还有一个容易被忽视的逆向场景:鸿蒙原生壳工程里嵌入 Flutter 页面。我们反馈列表页实际上也作为 SDK 形式提供给原生侧使用,这样后续如果有纯鸿蒙模块想复用反馈 UI,不用再写第二套。Flutter 侧要做的就是暴露一个统一的入口 Widget,原生侧通过 FlutterEngine 的注册表把它拉起。跨端方案的边界要提前划清楚,否则到时候这里补一块那里补一块,架构会非常乱。

2.4 part 到底用不用:工程化层面的一个反思

搜 Flutter 相关技术点的时候经常会看到part和part of这个语法。Dart 里的part允许把一个库拆到多个文件,主要用在某些代码生成场景,或者一个超大库内部做文件切分。但放在消息反馈模块这种常规业务代码里,我个人强烈建议不要用。

原因很简单:part会把一个文件的逻辑拆到多个物理文件中,但编辑器对它的跳转、重构、引用查找支持都不如常规 import 顺手。团队里来了新人,看到part 'feedback_widgets.dart'这种写法很容易懵,不知道这个文件到底从属于谁。现在的 Dart 工程规范是优先用 package 和 import 来做模块化,一个文件对应一个明确库边界。反馈模块的代码组织完全可以拆成 domain(模型与状态机)、repository(接口与缓存)、ui(页面与组件)三个独立目录,用 folder 维度组织比用 part 硬拆优雅得多。

判断标准就一句话:如果你用 part 只是为了“少写一个 import”,那说明模块划分本身出了问题,该做的是重新思考依赖关系,而不是用语言特性掩盖结构问题。

3. 反馈中心的交互与状态管理:Cubit、导航和 TabBar 那些细节

3.1 用 Cubit 管理异步反馈状态:为什么不是 Bloc

反馈列表页有典型的异步状态:加载中、加载成功、加载失败、空数据、下拉刷新。我们用flutter_bloc里的 Cubit,而不是完整的 Bloc。Cubit 和 Bloc 的区别很多人没想清楚——Bloc 多了 Event 到 State 的转换层,适合事件多、状态转换复杂的场景;而反馈列表的事件无非就是 load、refresh、retry,没有那么多需要记录的事件类型,用 Cubit 少写一堆 Event 类,代码更短也更好维护。

class FeedbackListCubit extends Cubit<FeedbackListState> { FeedbackListCubit(this._repo) : super(FeedbackListState.initial()); final FeedbackRepository _repo; Future<void> load({bool refresh = false}) async { if (state.isLoading && !refresh) return; emit(state.copyWith(isLoading: true, error: null)); try { final list = await _repo.fetchFeedbackList(); emit(state.copyWith(list: list, isLoading: false, isFirstLoading: false)); } catch (e) { emit(state.copyWith( isLoading: false, isFirstLoading: false, error: e.toString(), )); } } }

这里需要注意一个微观问题:避免重复请求。列表页在下拉刷新和首帧加载同时触发时,Cubit 里没有对“请求中是否还能再进请求”做保护的话,会出现列表抖动。我给 load 加了if (state.isLoading && !refresh) return,实测下来很稳。另外页面销毁时一定要调用cubit.close(),否则异步请求回来之后 emit 到一个已经 dispose 的 state 上,控制台会报BlocProvider相关的错误。这个在 Flutter 里属于“不报大错但很烦”的典型问题。

3.2 底部 Tab 切换不丢状态:IndexedStack 与 KeepAlive 实战

反馈模块在 APP 里的位置是底部四个 Tab 之一。底部 Tab 之间切换的时候,如果每个 Tab 都是独立 Route,切走再切回来页面会重建,反馈列表的滚动位置和已加载数据全丢。实际项目里我们用IndexedStack解决:

Scaffold( body: IndexedStack( index: _currentIndex, children: const [ HomePage(), FeedbackCenterPage(), CommunityPage(), MinePage(), ], ), bottomNavigationBar: _buildBottomBar(), );

IndexedStack 的原理是同时把这四个页面都 build 出来并保持存活,切换只是改 index,所以状态天然不丢。代价是一开始会多消耗一点内存和构建时间,但四个页面都是常规列表页面,完全可接受。

还有一种场景是在单个 Tab 内部做了列表和详情两个页面,用Navigator.push进详情再返回,列表位置恢复问题。Flutter 里PageRoute默认会保留下层页面的 State,但如果你在 push 之前手动做了dispose或者列表里用了 ListView.builder 又没有指定PageStorageKey,滚动位置还是会丢。正确姿势是给 ListView 加上显式的 key:

ListView.separated( key: const PageStorageKey<String>('feedback_list_scroll'), itemCount: items.length, ... )

配合AutomaticKeepAliveClientMixin,确保列表滚动位置在系统回收页面资源时也能恢复。如果项目里真的遇到了“从详情页返回列表回到顶部”的诡异问题,先检查这两样东西,八成能解决。

3.3 TabBar 点击动画取消与反馈列表的交互细节

反馈中心页面顶部有“全部 / 待处理 / 已解决”三个分类 Tab,产品当时要求切换时不要有那种默认的滑动指示器动画,看起来更干脆一些。Flutter 默认 TabBar 的 indicator 会跟随手势做滑动过渡,想取消,最快的方案是自定义一个BoxDecoration,并覆盖indicatorSize,让指示器不再随 Tab 滑动。

如果还想更彻底,那就别用 TabBar 默认样式,干脆用Row + GestureDetector自己画分类切换。我们对反馈中心的三个分类就是这么做的,切分类时用 AnimatedContainer 做一个 150ms 的透明度过渡,而不是默认的左右滑动指示器。效果干净,也不会有动画“拖泥带水”的感觉。

列表交互上还有一个高频问题:分类切换后旧请求结果覆盖新列表。用户先进入“全部”分类,列表请求还没回来,马上切到“待处理”,第二个请求先发出去了,结果第一个请求更慢、后返回,列表被旧数据覆盖。解决办法有三个,任选其一即可:加请求序号、加 AbortController 语义的取消标识、或者给请求附上当前分类参数并在回调里比对参数是否一致。我们在 Cubit 里塞了一个_latestRequestCategory,返回后和当前分类比对,不一致就直接丢弃。

3.4 组件通信选型:回调、总线还是状态管理

“flutter 组件通信”是高频搜索词,反馈模块里也确实会遇到这类需求:反馈详情页点“催一催”,希望列表页刷新订单状态;输入框内容变化了,希望提交按钮的高亮状态同步变化。面对这么多通信方式,我的选型决策表是这样的:

  • 父子组件直接同屏:优先用构造函数传参 + 回调,最直观,不用引入任何额外概念;
  • 同一模块内跨页面:优先用 Cubit/Provider 这类全局状态,因为反馈详情页和列表页本身共享同一份 FeedbackListCubit,通过BlocProvider共享即可;
  • 完全不知道谁在监听、且可能会被多个模块复用的消息:才考虑用 EventBus 或广播总线。比如后端推送过来的“反馈状态变化”,我们走的是 EventChannel 转成 Dart 事件,再通过内存总线分发到红点和列表两个位置。

EventBus 用起来爽但代价也明显:事件监听必须手动移除,某个页面忘记 cancel 订阅,页面销毁后事件回调还在执行,轻则内存泄漏重则空指针崩溃。所以我的原则是:能绕过总线就不上总线。反馈模块里真正需要总线的事件一只手数得过来,绝大部分通信都被构造参数和 Cubit 吸收掉了。

4. 鸿蒙适配的十个坑和排查套路:从环境到插件再到性能

4.1 环境搭建:SDK 分支、版本匹配与“not supported”报错

鸿蒙适配第一阶段往往卡在环境而不是代码。网上经常搜到一条报错,大意是当前配置的 Flutter SDK 不被支持、让你升级到某个已知支持版本。这个报错绝大多数情况是:你本地装的是 Flutter 官方稳定版 SDK,却把设备目标设成了鸿蒙,导致工具链校验失败。解决办法是切换到 OpenHarmony 的 Flutter SDK fork,并且保证 fork 版本和鸿蒙引擎版本、IDE 插件版本三者对齐。

我的建议是先用官方或社区提供的鸿蒙 demo 工程把环境验证通,再往里面塞业务代码。环境验证要包含三件事:flutter doctor能识别鸿蒙设备、空工程能跑到鸿蒙模拟器、原生混合工程能正常编译出 HAP 包。这三件事任何一个不过,后面全白搭。另外注意本地同时装了多个 Flutter SDK 时,路径配错是家常便饭,最好在项目里用fvm这类工具锁定 Flutter 版本,避免“在我电脑上能跑,到你电脑上就报 not supported”的经典问题。

4.2 第三方插件鸿蒙适配:一次完整的移植流程

消息反馈里用到的图片上传 SDK,在鸿蒙侧并没有现成实现。我们当时走了一遍 Federated Plugin 的完整适配流程,这里整理出来给后面的人参考:

  1. 先查插件仓库有没有 ohos 目录或已经声明的鸿蒙实现,没有的话基本要自己 fork;
  2. 分析 Dart 侧公开 API 一共暴露了哪些方法,把它们整理成一张清单,对应 Android 原生实现逐行看逻辑;
  3. 在 fork 出来的仓库里新建鸿蒙原生实现,用 ArkTS 重写清单上的方法,保持方法名、入参、出参和原有一致;
  4. 在插件 pubspec.yaml 里补充鸿蒙平台声明,把原生入口类名指向 ArkTS 实现;
  5. 本地业务工程通过 git 依赖或 path 依赖引用这个 fork 版本,跑通单元和集成验证。

移植过程中发现,很多插件的 Android 实现里依赖了 Android 特有的 API,比如 ContentResolver、Activity context 等,ArkTS 里没有对应概念。遇到这种,不能强行照搬,而是要看这个能力在鸿蒙上应该用什么系统 API 替代,本质上是做一次能力映射而不是代码翻译。图片选择、通知提醒、网络上传这三类能力,鸿蒙系统 API 覆盖度都比较高,适配难度不大;真正卡人的反而是那些依赖了 Android 内部私有 API 的冷门插件,这种直接放弃插件、自建轻量桥接,性价比更高。

4.3 首帧性能与 Impeller:反馈入口点开的快慢

反馈中心入口在首页底部 Tab,用户点进来如果首帧要两秒,再好的交互设计都白搭。我们在鸿蒙设备上做首帧优化主要动了三刀:

第一刀,列表首屏只加载必要数据。反馈列表第一屏最多显示 10 条左右,接口返回 20 条以上时剩余数据用滚动加载拉取,绝不一次性全部渲染。第二刀,图片做渐进式加载。列表里的报修图片统一用缩略图 URL,点开详情再看原图,这能省掉大量流量和内存。第三刀,延迟初始化非必要插件。不在 APP 启动时就初始化所有原生通道,哪个模块用到了再建,避免启动阶段被原生逻辑卡住主线程。

Flutter 新版本默认启用 Impeller 渲染引擎,我们也在鸿蒙适配分支上验证过。Impeller 带给体感最明显的变化是滑动列表时不再有那种“毛边”感。但如果遇到渲染表现异常,比如某些复杂蒙层闪烁,就需要检查当前引擎分支是否完整支持 Impeller,必要时通过引擎参数回退到 Skia 渲染。不要一上来就否定新引擎,先对比同一台设备、同一场景下的渲染输出差异再决定。

4.4 项目排障速查表:十条高频问题对照

鸿蒙适配期间我们积累了不少问题,整理成一张速查表,按“现象 → 可能原因 → 处理办法”的顺序排好,后续直接照着查:

现象可能原因处理办法
EventChannel 收不到推送事件Dart 侧订阅太晚,或原生侧未做事件缓存原生侧 onListen 时补发最近一条事件;Dart 侧全局订阅
MethodChannel 调用无响应通道名不一致,或原生侧未在正确生命周期注册统一维护一份通道常量表,注册放在 Ability 生命周期早期
插件报 MissingPluginException插件没有鸿蒙平台实现走 4.2 的适配流程自己补实现
分类 Tab 切换后列表被旧数据覆盖未校验请求参数与当前状态一致性在 Cubit 里比对请求参数,不一致直接丢弃结果
从详情返回列表后滚动位置丢失ListView 缺少 PageStorageKey给 ListView 指定稳定的 PageStorageKey
PlatformView 白屏原生地图控件在 Flutter 重绘时被销毁PlatformView 外层固定尺寸,不做 Flutter 侧变换
图片选择器打不开相册权限未正确声明或权限弹窗时序不对先调权限,再触发相册,不要合并成一次调用
上传大图后通道卡死通过 MethodChannel 传了字节数组原生侧压缩后写临时文件,只传路径
SDK 报 not supportedFlutter 版本和鸿蒙引擎分支不匹配锁定 fork 版本,用 fvm 统一版本
编译报 Gradle 插件应用方式错误新老 Flutter 工程结构混用,仓库还沿用 apply 方式引入插件改成新工程的 plugins DSL 方式,统一插件声明格式

排障的核心经验就一句话:先确认问题发生在哪一层。Flutter 页面的问题、Dart 侧逻辑问题、原生侧能力问题,这三类问题的排查思路完全不一样。用日志把 Dart 侧和原生侧都打出来,比对事件流是否一致,往往比闷头改代码快得多。

另外,反馈模块的日志埋点一定要做得比普通页面重。用户提交反馈、状态变更、超时催办、重新追问,这些关键动作都要有独立的埋点,才能回答产品最关心的“反馈处理时长分布”和“各分类反馈量趋势”。鸿蒙适配过程中,日志通道同样要先跑通,否则原生侧推送到达了没有日志,排障直接抓瞎。

如果让我重新做一遍这个项目,我会把验证顺序调成:先把消息反馈闭环在 Android 端完整跑通,再进入鸿蒙适配;而鸿蒙适配的第一周就应该集中火力验证 EventChannel、PlatformView、推送通道这三条关键链路,而不是等整个页面写完再切平台。消息反馈系统不是一个高难度业务系统,它的难点全在“把用户到处理人之间那条链路真正接通”上,链路通了,后面都是优化问题。踩过这些坑之后,我对 Flutter 跨端到鸿蒙的信心反而更足了,前提是每一步都提前把原生能力清单列出来,不要抱着“跑起来再补”的侥幸心理。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询