ARTICLE DETAIL

资讯详情

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

Flutter for OpenHarmony实战:Dart面向对象重构与跨端适配经验

Flutter for OpenHarmony实战:Dart面向对象重构与跨端适配经验 我从一个比较偏门的场景说起手头有个 Flutter 应用业务本身不复杂但要在 OpenHarmony 设备上跑起来。一开始我以为难点全在环境搭建、引擎适配、签名打包这些“体力活”上真正动手才发现最耗精力的反而是 Dart 端的代码组织。同一个页面类同一个数据模型在 Android 上写一套、在 OpenHarmony 上又要调整一套改到后面你会有一种感觉不是 Flutter 跨端能力不行是自己压根没用“面向对象”的方式设计代码。这篇文章就以 Flutter for OpenHarmony 实战为背景把我实际重构 Dart 类、整理继承关系、处理 Stream 数据流、以及踩过的一堆构建和状态坑都摊开来讲。适合正在做 Flutter 跨端适配的开发者也适合那些刚接触 Dart 面向对象、想搞明白“类和接口到底怎么划分”的初学者。1. 整体设计为什么 OpenHarmony 适配会把“Dart 类设计”逼成核心问题1.1 OpenHarmony 版 Flutter 的适配本质不是重新编译是重新分层先说一个很多人容易误解的地方OpenHarmony 上的 Flutter 并不是把 Android 的 Flutter Engine 完整挪过去而是社区和厂商通过移植层把 Flutter Engine 的底层能力接到 OpenHarmony 的图形栈、事件分发和平台通道上。这意味着你的业务代码大概率还是 Dart但“碰系统”的那一层——文件读写、网络状态、通知、传感器——全都会变成需要通过 Flutter 的 platform channel 去调用 OpenHarmony 侧的逻辑。这里有个连锁反应如果你早期写代码时把系统调用直接塞在 Widget 的 build 方法里或者把平台通道返回值到处散落那适配 OpenHarmony 的时候就不是“改几行 Channel 代码”那么简单而是要把所有跟平台相关的逻辑重新收拢、抽类、定义接口。换句话说OpenHarmony 适配逼着你先做一次彻底的面向对象重构。类边界清晰、依赖关系单一后面替换底层实现才可能只改一个工厂类而不是满屏幕搜索 setState。我实测下来比较稳的做法是把页面分为三层——表现层Widget、状态层State/Bloc、平台层Repository/Service。平台层一律通过抽象接口暴露给上层不直接暴露 MethodChannel。这样如果今天跑在 Android明天跑在 OpenHarmony你只需要新增一个平台的 Repository 实现然后在一个顶层工厂里面用条件编译或平台判断选择实例。1.2 面向对象不是“语法装饰”它决定你跨端维护成本很多 Flutter 教程教 Dart 面向对象时只讲“class”“extends”“implements”这些关键字怎么用然后甩两个例子就完事。但真到 OpenHarmony 适配阶段你才会发现面向对象设计是跨端维护成本的分水岭。举一个真实场景我原本写了一个下载任务管理组件DataModel 直接用了 Map 来存任务状态Widget 层通过 key 去取。刚开始很爽因为不用定义类。后来要适配 OpenHarmony 的 FTP 能力需要更换下载库和协议解析方式问题一下爆发了因为任务状态散落在 Map 里状态机逻辑和 UI 渲染强耦合改底层协议时 Widget 层也要跟着动而且根本不知道哪些页面在使用哪个字段。最后我花了一个晚上把所有 Map 改成强类型类定义 TaskStatus 枚举用抽象类约束 TaskRepository再按 Android 和 OpenHarmony 分别做实现。改完之后底层替换一个库只需要动几十行UI 层完全不需要感知。我并不是说 Map 绝对不能用但一旦字段被多个页面共享或者存在状态流转就应该用类去封装。这类经验在普通单端开发里不容易暴露多端适配时才会被逼出来。所以这篇文章我会强调“类的粒度怎么切、抽象接口怎么定、Mixin 怎么用”这些都是实战里真正决定效率的东西。2. Dart 面向对象核心细节解析类、继承、Mixin 与模块拆分2.1 构造函数与初始化列表少写“手工赋值”,多利用语法糖Dart 的类写起来其实很爽因为有大量语法糖可以省掉重复代码。最容易让人忽略但也最实用的就是初始化列表initializer list。不像 Java 里你得在构造方法体里一行行 this.name nameDart 里直接用class TaskModel { final String id; final String title; final TaskStatus status; TaskModel({ required this.id, required this.title, this.status TaskStatus.pending, }); }这段代码看着简单但背后有几层含义所有字段用 final代表这个类的实例是不可变的会在多端数据流传递中减少很多“不知道谁改了我状态”的坑required 关键字让编译器在实例化时就强制你传参避免运行到某行才发现漏了字段默认值让某些可选字段不需要每次传。这些都是面向对象设计中不可变对象的经典做法。OpenHarmony 适配时数据从原生侧通过 MethodChannel 传回来我几乎全部会先转成这种不可变的 Model 类再向下传递。Model 类一旦设计好后续序列化、单元测试、日志打印都会顺畅很多。再强调一个容易被忽略的语法命名构造函数。它能让“创建对象”这件事具备语义化。比如日期时间解析可以直接定义TaskModel.fromJson(MapString, dynamic json)而不是在外部写一堆 parse 方法。命名构造函数配合工厂构造函数是 Dart 类设计里很有价值的能力。尤其从 OpenHarmony 侧返回的数据往往是 JSON 结构你在 Model 类里内置 fromJson 和 toJson整个数据流会非常整洁。2.2 继承与多态的边界为什么“接口优先”而不是“基类优先”Dart 只有单继承但支持任意多个接口implements和混入mixin。很多从 Java 转过来的开发者习惯先做个 BasePage、BaseRepository 这类基类然后所有子类继承它。我不建议滥用基类继承因为基类一旦膨胀职责不清晰子类会被拖着走。OpenHarmony 适配过程中平台差异最大的问题就是接口差异Android 有某些通道能力OpenHarmony 可能没有两边返回的数据格式也可能不一样。如果前置用抽象接口约定好能力边界具体差异在各平台的实现类里消化整个架构会稳很多。举个例子abstract class FileRepository { FutureString getCachePath(); FutureListString listFiles(String path); Futurebool deleteFile(String path); } class AndroidFileRepository implements FileRepository { override FutureString getCachePath() async { final path await _channel.invokeMethod(getCachePath); return path as String; } // ... } class OhosFileRepository implements FileRepository { // 针对 OpenHarmony 的实现内部可能调不同通道名 }这里的核心是FileRepository抽象类只定义“能做什么”不定义“怎么做”。上层页面拿到的是这个抽象类型调用方根本不 care 底下跑的是 Android 还是 OpenHarmony。这种模式在 Flutter 官方插件开发中也很常见只是业务代码里很多人偷懒没这么写到了多端适配才还债。2.3 “part”与库拆分什么时候用什么时候别用热词里有 “flutter 中 part”这也是很多 Dart 初学者一搜索就会碰到的东西。Dart 的part指令允许把一个库拆分成多个文件配合part of来共享私有成员。但它是一把双刃剑用得好可以有效拆分巨类用不好会形成隐式的全局耦合让团队维护很痛苦。我个人的实践结论是part只在特定场景用——比如同一个状态机类的多个私有方法非常长想按方法类别拆分到不同文件但又希望它们能访问同一个私有状态。这种场景如果用常规的import拆分会面临私有变量无法访问的问题被迫用 getter/setter 扩大暴露面反而把封装破坏了。如果你只是普通类的功能拆分建议优先用独立的库文件加公开接口组合不要动不动就上 part。因为 part 文件之间的依赖关系在 IDE 和静态分析工具里显示得不够直观新人接手时常常会迷茫“这个类到底怎么拼起来的”。我看过有些项目一个 library 下面挂了七八个 part 文件最后连自己人都说不清楚边界。Dart 的“每个文件默认就是一个独立 library”这种设计本身就是鼓励高内聚低耦合的part 是特殊情况下的逃生口不是日常工具。2.4 Mixin把跨类复用的逻辑“组合”进类里讲到面向对象Dart 里最有特色的就是 Mixin。它解决的痛点是你有几个类都需要同一个行为但它们不应该是父子继承关系。比方说你有多个页面 Widget都希望具备“提交表单时统一校验并显示 loading”的行为但每个 Widget 的父类不能乱改这时候 Mixin 就能起作用。mixin FormSubmitMixinT on StateT { bool _submitting false; Futurevoid submitForm(Futurevoid Function() action) async { if (_submitting) return; _submitting true; setState(() {}); try { await action(); } finally { _submitting false; setState(() {}); } } }这样任何需要“防重复提交 自动 loading”的 State 类只要with FormSubmitMixin就能获得这个能力。注意我在 mixin 上限制了on StateT代表只能在特定父类型上使用这种约束让 mixin 的应用边界更清晰。实测下来mixin 用来做日志埋点、状态恢复、权限校验、页面路由工具扩展都很顺手。跟 OpenHarmony 适配结合时我会把平台相关的能力暴露做成抽象接口再用 mixin 给公共 Widget 注入“平台敏感行为”上层代码看起来干净实际能力又完整。3. 实操过程从建工程到用类封装一个跨端功能模块3.1 环境准备与创建 OpenHarmony Flutter 工程这里先提醒一句不同时间节点的安装方式差异比较大具体以你当前拿到的 SDK 版本的官方文档为准。实际操作大体分三步安装 Flutter SDK 并切换到目标分支。OpenHarmony 适配版 Flutter 不是官方主干而是从开源社区拉出来的独立版本。我建议下载后放独立目录不要跟正常的 Android 开发用同一个 Flutter SDK 混在一起因为版本切换会带来不必要的麻烦。准备 OpenHarmony 的 DevEco Studio 和 HarmonyOS SDK。这部分核心是签名和设备连接。早期的调试签名字符串需要手动填 profile现在新版本的 IDE 已经简化很多了但仍然建议先把模拟器跑通一遍再碰真机。创建工程时如果模板里没有 OpenHarmony 选项就先用 Flutter 默认模板创建再按社区提供的适配配置把目录结构和构建脚本调整过来。比较常见的坑是 Flutter 工程默认使用 Gradle 构建而 OpenHarmony 工程侧还要配合 DevEco 的项目结构。这时候你可能会在构建日志里看到类似 “you are applying flutters main gradle plugin imperatively using the apply” 这种提示还有 SDK 版本不支持之类的警告。这些我在下一节具体讲排查思路先记住一个原则拿到一个移植分支先跑自带的示例工程确认底通之后再搬业务代码不要一上来就指望着把 Android 工程一键迁移。3.2 场景实例把“任务列表页”改成面向对象风格我挑一个非常常见的业务场景来演示任务列表页。本来很多人会这么写class TaskPage extends StatefulWidget { override StateTaskPage createState() _TaskPageState(); } class _TaskPageState extends StateTaskPage { ListMapString, dynamic tasks []; // 加载逻辑... }这样写本身在 Android 上没问题但一旦要接 OpenHarmony 的数据源你就会发现ListMapString, dynamic这种结构编辑器没法给你任何字段提示底层如果把status从数字改成字符串页面层到处都要改。重构第一步定义强类型模型enum TaskStatus { pending, running, done, failed; static TaskStatus fromName(String name) { return TaskStatus.values.firstWhere( (e) e.name name, orElse: () TaskStatus.pending, ); } } class TaskModel { final String id; final String name; final TaskStatus status; final DateTime createdAt; const TaskModel({ required this.id, required this.name, required this.status, required this.createdAt, }); factory TaskModel.fromJson(MapString, dynamic json) { return TaskModel( id: json[id] as String, name: json[name] as String, status: TaskStatus.fromName(json[status] as String), createdAt: DateTime.parse(json[createdAt] as String), ); } }第二步定义仓库接口和实现abstract class TaskRepository { FutureListTaskModel fetchTasks(); } class OhosTaskRepository implements TaskRepository { static const _channel MethodChannel(com.example.ohos/task); override FutureListTaskModel fetchTasks() async { final list await _channel.invokeMethod(fetchTasks) as Listdynamic; return list .map((e) TaskModel.fromJson(e as MapString, dynamic)) .toList(); } }第三步页面 State 里只依赖抽象class _TaskPageState extends StateTaskPage { late final TaskRepository _repository; ListTaskModel _tasks []; override void initState() { super.initState(); _repository createTaskRepository(); _load(); } Futurevoid _load() async { final tasks await _repository.fetchTasks(); if (!mounted) return; setState(() _tasks tasks); } }createTaskRepository这个工厂函数内部按Platform.isAndroid或 OpenHarmony 环境判断返回不同实例。这样整个页面代码里再也没出现 MethodChannel、也没出现 JSON 解析底层不管怎么换界面几乎不动。这就是面向对象在实战中的价值不是说写个 class 就完事而是让依赖方向清晰替换成本可控。3.3 Stream 与 EventChannel从原生侧向 Flutter 推数据做跨端功能时除了主动拉数据还经常遇到原生侧主动上报的场景比如下载进度、传感器数据、系统事件。Flutter 侧的 EventChannel 就是干这个的。OpenHarmony 适配版同样支持 EventChannel但通道名、数据结构要注意跟原生实现保持一致。Dart 侧接收事件的经典姿势是用 Streamclass DownloadProgressService { final _controller StreamControllerDownloadProgressModel.broadcast(); final _eventChannel const EventChannel(com.example.ohos/download); StreamDownloadProgressModel get stream _controller.stream; void startListen() { _eventChannel.receiveBroadcastStream().listen((event) { final map event as Mapdynamic, dynamic; final progress DownloadProgressModel( taskId: map[taskId] as String, percent: (map[percent] as num).toDouble(), ); _controller.add(progress); }); } void dispose() { _controller.close(); } }这里有几个容易踩的坑一是StreamController忘记 close页面销毁后会引发内存泄漏在 OpenHarmony 上长时间跑更容易暴露二是 listen 回调里直接做耗时解析容易卡帧强烈建议在数据进入 Stream 时就完成模型转换UI 层拿到的一律是不可变的 Model 对象三是 EventChannel 的receiveBroadcastStream是广播流多个地方同时监听会产生重复逻辑最好由服务类统一接收再分发给多个Widget必要时内部用StreamController.broadcast。很多人在 Flutter 跨端开发后知后觉地用MethodChannel做“轮询式”获取进度这是很笨的办法。用 Stream 做事件驱动配合 Dart out-of-box 的异步能力才是更合理的方案。OpenHarmony 底层的 Binder 事件传递天然适合流式语义EventChannel 的桥接也是顺着这个思路来的。3.4 状态管理选型Bloc/Cubit 如何配合 Dart 类体系搜索热词里有一堆“flutter bloc教程”“flutter cubit”。我在这里只说和面向对象设计相关的部分。Bloc/Cubit 这类状态管理方案本质上就是让你把页面状态抽成独立的类用事件驱动状态变化再用 Stream/State 暴露给 UI。对于 OpenHarmony 适配场景状态类的价值在于把“页面怎么显示”和“业务状态怎么变”解耦了。Cubit 是最轻量的用法class TaskListCubit extends CubitTaskListState { final TaskRepository repository; TaskListCubit(this.repository) : super(TaskListState.initial()); Futurevoid refresh() async { emit(state.copyWith(loading: true)); try { final tasks await repository.fetchTasks(); emit(state.copyWith(loading: false, tasks: tasks)); } catch (e) { emit(state.copyWith( loading: false, error: $e)); } } }你发现没有这里有个很好的实践Cubit 本身并不知道数据从哪来它依赖的是 TaskRepository 抽象。于是同一个 Cubit 既可以用在 Android 的 Flutter 页面也可以用在 OpenHarmony 的 Flutter 页面只需要在创建 Cubit 时注入不同的 Repository 实现。这种“依赖注入 面向接口”的组合是我认为 Flutter 跨端项目中性价比最高的套路。用过之后你就不想回到那种“一个页面里又调接口又管状态”的写法了。从类设计角度看还应该把 “state” 做成不可变类用 copyWith 产生新状态。这样在 Bloc 的调试工具里你可以清晰看到每次状态变化快照回溯问题非常方便。OpenHarmony 调试工具链没有 Android 生态那么完善所以这种边界清晰的状态类反而是自制调试利器。4. 常见问题与排查技巧实录构建、状态丢失与渲染兼容4.1 构建脚本和 Gradle 警告别忽略但也没必要慌我在前面提到过OpenHarmony Flutter 工程构建时很容易冒出一堆警告。最常见的是这条类似“you are applying flutters main gradle plugin imperatively using the apply”的提示以及“current configured Flutter SDK is not known to be fully supported”。这一般是描述性警告意思是你的构建脚本里用了命令式 apply 方式而不是新版插件式配置。大多数情况下能正常构建不用恐慌。教你一个排查顺序先在命令行执行flutter doctor看 Flutter SDK 和依赖是否识别正常。再看 OpenHarmony 侧的工程构建日志确认报错是出自 Gradle 还是 DevEco 的编译链路。如果构建真的中断优先检查 Flutter SDK 版本跟移植分支是否匹配。很多适配分支要求特定的 Flutter commit 或 tag版本不一致导致的报错会非常魔幻。最后才考虑是不是本机环境问题比如缓存目录权限、NDK 版本、Java 版本。我把这个顺序写在前面是发现大多数新人一看到 Gradle 警告就跑去试各种“clean rebuild”不仅累还解决不了问题。构建日志要分段看第一段是环境探测第二段才是实际编译错误。你真正要关注的是编译错误往往可以直接定位到某个 Dart 文件或者原生代码文件。4.2 Navigator 切换页面后状态丢失先检查是不是“类没设计好”热词里有句话是“flutter navigator切换页面后会丢失状态吗”。这是个老话题也跟 Dart 类设计有点关系。默认情况下Navigator push 到新页面后旧页面 State 会被保留但如果用了一些状态管理库或者你在页面销毁时调用了 dispose那状态丢失就是你主动造成的。OpenHarmony 适配场景里更常见的是页面被切换到后台或者原生端把 Flutter 容器给回收了Dart 端 State 全部重建。要解决这类问题第一件事是不要依赖 Widget 的 State 来保存业务数据。业务数据应该存在于 Repository 或 Bloc/Cubit 中页面 State 只负责渲染。这样即使页面重建只要 Bloc 实例还在状态就不会丢。怎么保证 Bloc 实例在页面切换期间不被销毁你可以把它注册在根级 Injector 里或者挂到路由顶层。我现在写 Flutter几乎不会为了方便把生命周期敏感的数据放在 State 里。跨端环境下页面生命周期事件并不完全跟 Android 一致把数据迁出 State 才是稳妥的面向对象设计。另外如果你用了 PageView 或者 IndexedStack这些组件默认会保留相邻页面 State但如果为了性能设置了 allowImplicitScrolling会改变预加载行为。这种情况下页面重建后的恢复逻辑要走“初始化时从 Repository 拉取”的老路。所以别把“状态不丢”寄托在 Flutter 框架某个特性上而是从数据流设计上保证可恢复。4.3 Impeller 渲染带来的“画面不更新”错觉热词里有 flutter impeller。Impeller 是 Flutter 新渲染引擎的名字它对 GPU 友好的同时也可能带来一些旧工程显示异常。常见现象是页面某个 Widget 更新了但屏幕上看起来没变或者有残影。遇到这种问题先别怀疑自己代码写错先检查是不是 Impeller 渲染下的 shader 缓存问题。最简单的方法是跑一下flutter run --enable-software-rendering看看是否消失如果消失通常就是渲染管线兼容问题。OpenHarmony 适配版 Flutter 使用的图形栈未必完整支持 Impeller 在新平台上应有的特性所以有些提交在 Android 正常、在 OpenHarmony 上显示异常。调试技巧上可以在MaterialApp里设置builder加一层全局容器用来强制刷新 key快速定位是“数据没变”还是“渲染没刷新”。而在生产环境我会更依赖明确的 RepaintBoundary 来隔离重绘区域。不要过度使用因为每个 RepaintBoundary 都会增加内存开销只加在确实需要独立重绘的组件上。另一个容易踩的坑是 TabBar 点击切换时的动画取消。有人搜“flutter tabbar点击取消动画效果”其实是希望保留页面状态又不想看到不必要的切换动画。这不是复杂问题但跟 Impeller 配合时动画回调的处理可能会因为 VSync 信号不同步而出现奇怪的视觉跳跃。你可以给 TabBar 设置animationDuration: Duration.zero同时用AutomaticKeepAliveClientMixin保留页面状态这俩配合起来很有效。4.4 原生跳转和平台通道OpenHarmony 侧不要照搬 Android 实现最后讲一个最容易踩到“大坑”的适配点希望从 Flutter 跳转到原生 Activity。Android 里套路很成熟MethodChannel 调原生端原生端用 startActivity 拉起新页面。但 OpenHarmony 侧的 UI 组件体系跟 Android Activity 完全不同它的页面体系更接近“Ability”或“ArkUI 页面”跳转方式、参数传递、返回结果协议都有差异。我建议的做法是在 Dart 侧定义抽象的NativeNavigator接口提供push(String pageName, MapString, dynamic args)方法。Android 实现通过 Activity 之间的 Intent 跳转OpenHarmony 实现通过对应的 Ability 跳转。上层统一用这套接口不做任何端侧判断。实际写的时候要特别注意参数序列化OpenHarmony 的跨进程传递不像 Android 那样能塞 Parcelable大部分场景只适合传可序列化的基础类型。如果你传的是自定义对象最好先toJson()转成 Map再传过去避免在通道层出现“类型不支持”的诡异报错。这类问题的根源就是一开始没有把平台差异隔离开。如果你在业务代码里到处直接写 MethodChannel那么每换一个平台都要全局搜索通道名然后逐段改。而用接口 工厂模式一次适配后续再做第三端接入时就轻车熟路了。这也是我这篇文章反复强调面向对象的原因——它不是写在简历上的知识点而是跨端工程真正的减负工具。5. 我个人实操中的体会类边界先画清楚再动手写代码这篇内容写到尾声我不太想总结什么“核心要点”就说一点真实体会。以前我做 Flutter 开发很习惯拿到需求就开写类不类的等代码多了再顺手抽一抽。但经过 OpenHarmony 适配这个项目我被强制扭转了习惯跨端工程的每一层都要先问一句“这个能力是哪一端提供的如果换了平台这部分代码会不会被动”只要会那就值得定义一个抽象接口值得建一个模型类。我后来接手任何 Flutter 项目第一步都不是看页面长什么样而是把项目里的 class 文件一个个扫过去看数据流向是谁在依赖谁。如果发现一个页面类理直气壮地直接拿 Map 到处取字段我会评估重构成本如果发现 platform channel 调用散落在各个 Widget 里我会建议先收拢。这个项目之后我对自己代码的要求也变成了构造函数尽量用不可变字段、状态数据不堆在 State 里、什么功能该用 mixin 什么功能该下沉到抽象类全都要有明确理由。最后分享一个具体可落地的小技巧你在定义抽象接口时可以把接口文件名统一叫xxx_repository.dart或者xxx_service.dart实现类按平台放不同目录比如android/和ohos/。这样单看工程目录结构就能感知到这个项目的扩展边界在哪里。以后再有人说要支持新的平台你打开目录照着老模式加一个实现类基本不会有额外的惊吓。这套东西不复杂但它恰恰是我在 OpenHarmony 实战里被折腾得最痛之后最想告诉别人的经验。
返回列表