Flutter适配OpenHarmony实战:手语App分类列表跨端开发全记录
发布时间:2026/9/30 15:07:41来源:尧图网络
说实话在真正把项目跑在 OpenHarmony 设备之前我对“Flutter 写鸿蒙应用”这件事是持保留态度的。毕竟 OpenHarmony 不是安卓UI 框架、打包产物、权限模型、原生通道全都不一样传统 Flutter 开发那套经验不可能直接平移。但等到我把这个手语学习 App 迁移到 flutter_for_openharmony 分支并把分类列表模块完整实现之后我必须说一句这套路真能走通而且走通之后的收益非常大——一套 Dart 代码既能出安卓包也能打 HAP鸿蒙应用包UI 交互、状态管理、数据层几乎零改动。这篇文章就把我这次实战的完整过程写出来。核心围绕手语学习场景里最重要也最容易写砸的部分——分类列表一级分类怎么设计、二级条目列表怎么展示、点击切换如何保状态、原生视频能力怎么通过通道接进来、OpenHarmony 环境下又会踩到哪些典型坑。内容包括环境搭建、数据建模、UI 实现、鸿蒙适配、问题排查五个大块适合正好想做 Flutter 鸿蒙适配的小伙伴也适合正在实现“分类-列表-详情”这种经典结构但总觉得差点意思的人。1. 手语学习APP的整体设计与思路1.1 为什么是“Flutter OpenHarmony”这个组合手语学习这个场景本身就有很强的多端属性。用户可能在手机上学在平板上看视频分解教育机构和公益组织还经常要部署到大屏一体机上。如果每一端都单独写一套原声应用成本和维护量直接爆炸这也是我一开始就锁定 Flutter 的原因UI 层、业务层、状态层全都可以复用唯一要动的只是平台能力接入层。OpenHarmony 这边的形势也很明确。作为开源底座它已经能跑在手机、平板、开发板、智能大屏上而且跨设备能力本身就是它的主场。手语学习恰好在这些设备上都有需求这就让“一次编码、多端出包”的价值被拉满了。官方社区维护了 flutter_flutter 的 OpenHarmony 分支提供了 HAP 的构建通道、ohos 平台工程模板以及 Flutter 引擎在 OHOS 上的适配。换句话说Flutter 不是只能在 Android 上跑只要你把 SDK 切换到这个分支整个工具链就会认识 OpenHarmony 的构建产物和平台目录。当然这个组合也不是没有代价。相比安卓的成熟插件生态OpenHarmony 上的 Flutter 插件目前还有不少洞要补尤其是音视频播放、摄像头这些依赖原声能力的模块。所以我在项目一开始就做了个决策凡是 UI 和业务逻辑必须纯 Dart 实现凡是原生能力统一抽到平台通道层后续逐个补适配。这个决策在后面帮了大忙分类列表模块基本没有因为鸿蒙适配改过一行 UI 代码。1.2 手语分类功能到底要做什么手语学习 App 的核心不是罗列一堆手语词汇而是要让用户按照合理的路径去学。分类列表在这里扮演的其实是“学习地图”的角色。我把整个内容体系分成了三个层级第一层是分类页按场景划分比如日常问候、家庭称谓、餐饮食物、医疗求助、交通出行、数字字母。每个分类需要展示名称、一句话描述、图标配色、条目数量以及该分类下的学习进度。第二层是分类下的手语条目列表一个条目就是一个手语词比如“谢谢”“生病了”“停车”。每条需要展示词名、手语视频封面、简短说明、难度标记。第三层是条目详情页播放手语视频展示分步动作拆解和文字说明。分类列表如果只是用 ListView 堆一行行文字产品上是不过关的。它承担了两个重要任务一是让用户一眼知道有哪些学习主题二是用卡片、进度、颜色这些视觉元素降低学习门槛。所以我最终采用了“首屏拉伸网格 二级卡片列表”的结构分类页用带渐变色的大卡片网格排列点击后进入二级条目列表列表项高度统一、内容紧凑方便在平板上也保持良好的浏览节奏。1.3 项目模块与工程结构这个项目的工程结构完全围绕 feature 来组织没有用传统的“页面-组件-工具”三层而是把领域功能独立成包lib/features/category/分类列表的模型、仓库、状态管理、UIlib/features/detail/条目详情页与播放逻辑lib/common/通用组件、颜色、尺寸、工具类lib/platform/channels.dart集中定义所有 MethodChannel 和 EventChannel 名称ohos/OpenHarmony 工程目录由分支工具链生成原生桥接代码在里面。这样组织的最大好处是后续如果要加“手语测验”“每日一句”功能每个功能都能独立开发、独立测试不会侵蚀彼此的文件。尤其是分类列表这种偏数据驱动的模块把数据层和 UI 层解耦之后我改数据模型不影响页面改页面布局不影响状态管理调试体验顺畅很多。2. OpenHarmony端Flutter工程环境与初始化2.1 环境准备与工具链版本先说环境。想让 Flutter 认识 OpenHarmony你需要做的第一件事就是把 Flutter SDK 切换成 flutter_for_openharmony 对应的分支这一步不能跳过。官方上架 OpenHarmony 的 SDK 下载同样走官方镜像或 FTP 源直接拉到本地后解压然后把bin目录加进PATH。操作系统推荐 Linux 或 macOSWindows 也能编译但 OpenHarmony 侧工具链在 Windows 上偶尔会有路径长度和符号链接问题DevEco Studio 需要安装并配置好 OpenHarmony SDK 的路径确认本机 Java、Git 环境正常通过flutter --version检查看到分支名或者版本号带 ohos 标识说明切换成功。环境搭建里最容易出问题的不是 Flutter 本身而是版本匹配。OpenHarmony 分支一般会锁定某个 Flutter 基线版本比如当前稳定驱动通常对应 3.22 或 3.24 这条线不会追官方主线最新的版本。这个不是落后而是因为引擎适配需要时间。你如果从主工程切过来记得把pubspec.yaml里的依赖版本整体跑一遍flutter pub upgrade式检查避免某些包引用了新 SDK 才有的 API。2.2 创建工程与 AS 快速上手手语学习 App 的工程创建我强烈建议用命令行而不是 IDE 向导。因为分支工具链对ohos平台的支持在环境变量和 SDK 路径上更敏感命令行跑会打出更直观的日志。flutter create ./sign_language --project-name sign_language --org com.example.sign工程生成后在项目根目录执行flutter create --platformsohos .如果当前分支注册了 ohos 平台这一步会在工程下生成ohos/目录。这个目录是 DevEco Studio 工程HAP 的编译入口就在这里。然后打开 DevEco Studio方式跟 Android Studio 打开安卓工程类似File - Open选择项目根目录下的ohos文件夹。IDE 会自动同步 Gradle 配置。第一次同步会拉取 OpenHarmony SDK 和构建插件耗时根据网络情况不定建议边休息边等。同步完成后你就能在 IDE 里直接调试或者打包了。如果习惯用 Android Studio 来管理 Flutter 代码那也没问题——Flutter 代码、pubspec.yaml、Dart 侧调试都在 AS 里操作只把ohos/目录当作原生工程丢给 DevEco。两条腿走路互不干扰。2.3 依赖与插件的“鸿蒙化”检查这篇实战里项目依赖的插件并不多核心几个是flutter_bloc或者cubit做状态管理、dio做网络请求如果后续要远程加载词汇、video_player或者自定义平台通道播放视频。但你一定要注意检查每个插件在 pub 上是否有 ohos 平台的实现没有的话看插件是否纯 Dart 实现纯 Dart 的可以放心用依赖原生能力的要自己通过 MethodChannel 在ohos/entry/src/main/ets里补实现。我在这个项目里遇到的典型情况就是视频播放插件在 OpenHarmony 上不可用于是自己封装了一个OhosVideoPlugin用 MethodChannel 控制播放、暂停、跳转用 EventChannel 把播放进度和播放结束事件回传给 Dart 侧。class OhosVideoPlugin { static const MethodChannel _control MethodChannel(sign_language/video_control); static const EventChannel _events EventChannel(sign_language/video_events); Futurevoid play(String assetPath) async { await _control.invokeMethod(play, {path: assetPath}); } Streamdynamic get events _events.receiveBroadcastStream(); }原生侧用 OpenHarmony 的 AVPlayer 能力去实现同一个通道名。这样 Dart 侧完全不知道底层换了播放器对上层 UI 和列表模块来说就是一个普通插件。3. 分类列表的数据建模与状态管理3.1 手语分类的领域模型设计分类列表看起来简单但一旦数据层级没设计好后面改起来就是灾难。我先把模型拆开来看顶层是分类对象分类下面挂条目列表条目又关联视频资源和说明文本。class SignCategory { final String id; final String name; final String subtitle; final int colorValue; final String iconAsset; final ListSignItem items; SignCategory({ required this.id, required this.name, required this.subtitle, required this.colorValue, required this.iconAsset, required this.items, }); factory SignCategory.fromJson(MapString, dynamic json) { return SignCategory( id: json[id] as String, name: json[name] as String, subtitle: json[subtitle] as String? ?? , colorValue: json[color] as int? ?? 0xFF607D8B, iconAsset: json[icon] as String? ?? , items: (json[items] as Listdynamic? ?? []) .map((e) SignItem.fromJson(e as MapString, dynamic)) .toList(), ); } } class SignItem { final String id; final String name; final String videoAsset; final String description; final int difficulty; SignItem({ required this.id, required this.name, required this.videoAsset, required this.description, this.difficulty 1, }); factory SignItem.fromJson(MapString, dynamic json) { return SignItem( id: json[id] as String, name: json[name] as String, videoAsset: json[video] as String, description: json[description] as String? ?? , difficulty: json[difficulty] as int? ?? 1, ); } }模型设计上有几个关键点。第一id是稳定标识我强烈不建议拿中文名当 id后续如果要接后台、做进度上报中文名很容易因为编码或空格问题出幺蛾子。第二颜色值直接存在 JSON 里这样 UI 层可以根据分类动态配色不必硬编码映射表。第三条目数量不落库而是通过items.length实时计算避免两份数据不一致的问题。3.2 数据加载与 Repository 模式数据源我采用了 assets 本地 JSON原因很简单第一批手语内容量不大几十个分类加上两三百个条目本地加载速度快、离线可用、减少网络依赖。后续如果内容扩充到几千条再平滑迁移到网络接口即可前提是 Repository 接口形态不变。class CategoryRepository { FutureListSignCategory fetchCategories() async { final raw await rootBundle.loadString(assets/data/categories.json); final decoded json.decode(raw) as Listdynamic; return decoded .map((e) SignCategory.fromJson(e as MapString, dynamic)) .toList(); } }这里必须做一层 Repository。因为页面和状态层不应该知道数据来自本地还是网络Repository 是唯一的数据入口。如果后续接入在线内容只需要改 Repository 的实现页面代码一行都不用动。这也是我在实战里反复跟身边同事强调的不要在 Widget 里直接去 loadString不要跳过 Repository 层。JSON 文件放在assets/data/categories.json后记得在pubspec.yaml的flutter.assets段里声明目录否则运行时直接报找不到资源。flutter: assets: - assets/data/ - assets/images/ - assets/videos/3.3 状态管理选型与 Cubit 落地说到状态管理这个项目的分类列表有一个特点状态数量不多但状态切换比较频繁——加载中、加载成功、加载失败、选中某一个分类。用setState其实也能写但分类列表还涉及跨页面传递状态、返回后保持列表位置用原生的 setState 写起来会很别扭。我最后选了flutter_cubit也就是 bloc 库里的 Cubit 部分而没有直接用 Bloc。原因有三点。一是 Cubit 比 Bloc 简单不需要写 Event 类对于“加载数据、切换选中项”这种有限操作写 Event 纯属浪费二是 Cubit 基于状态类的机制天然适合跟BlocBuilder、BlocListener配合UI 层能精确控制重建范围三是纯 Dart 实现在 OpenHarmony 上没有任何原生依赖不用适配。enum CategoryStatus { initial, loading, loaded, failure } class CategoryState { final CategoryStatus status; final ListSignCategory categories; final String? errorMessage; const CategoryState({ this.status CategoryStatus.initial, this.categories const [], this.errorMessage, }); CategoryState copyWith({ CategoryStatus? status, ListSignCategory? categories, String? errorMessage, }) { return CategoryState( status: status ?? this.status, categories: categories ?? this.categories, errorMessage: errorMessage ?? this.errorMessage, ); } } class CategoryCubit extends CubitCategoryState { CategoryCubit(this._repository) : super(const CategoryState()); final CategoryRepository _repository; Futurevoid load() async { emit(state.copyWith(status: CategoryStatus.loading)); try { final result await _repository.fetchCategories(); emit(state.copyWith( status: CategoryStatus.loaded, categories: result, )); } catch (e) { emit(state.copyWith(status: CategoryStatus.failure, errorMessage: $e)); } } void selectCategory(String id) { final categories state.categories; if (categories.isEmpty) return; final index categories.indexWhere((c) c.id id); if (index -1) return; } }Cubit 落地的核心体会是状态类要不可变每次变更通过copyWith产生新实例。这不仅是惯例而是让 UI 正确重建的前提。BlocBuilder 是依靠状态对象是否发生变化来决定是否重建的如果直接改旧对象里的字段UI 不会响应。4. 分类列表的UI实现与交互开发4.1 分类网格与条目列表的双层结构分类首页我用的是CustomScrollView SliverGrid而不是最普通的 GridView。理由是要在顶部放一个搜索框和欢迎横幅用 CustomScrollView 可以把这些头部和网格列表放到同一个滚动体系里滑动联动非常自然。网格卡片尺寸在大屏和手机上都能保持合理密度我使用了SliverGridDelegateWithMaxCrossAxisExtent限定每列最大宽度这样屏幕变宽时自动增加列数。class CategoryHomeView extends StatelessWidget { const CategoryHomeView({super.key, required this.cubit}); final CategoryCubit cubit; override Widget build(BuildContext context) { return BlocBuilderCategoryCubit, CategoryState( bloc: cubit, builder: (context, state) { if (state.status CategoryStatus.loading) { return const Center(child: CircularProgressIndicator()); } if (state.status CategoryStatus.failure) { return _ErrorRetryView( message: state.errorMessage ?? 加载失败, onRetry: cubit.load, ); } return CustomScrollView( slivers: [ const SliverToBoxAdapter(child: _HomeHeader()), SliverPadding( padding: const EdgeInsets.all(16), sliver: SliverGrid( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 200, mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: 0.92, ), delegate: SliverChildBuilderDelegate( (context, index) _CategoryCard( category: state.categories[index], onTap: () _openCategory(context, state.categories[index]), ), childCount: state.categories.length, ), ), ), ], ); }, ); } }进入分类后二级列表用ListView.separated条目卡片高度统一。这里有个细节卡片高度如果不定视频封面和文字说明会导致列表项高度跳跃滑动时卡顿明显。所以我给条目卡片固定了一个标准高度描述最多两行超出部分省略号处理。4.2 卡片设计与 TabBar 点击动画处理分类卡片我做了比较重的视觉设计线性渐变背景、左上角图标、左下角分类名和条目数、右上角一个小的进度百分比。渐变色从模型里的colorValue派生相邻两个卡片颜色体系来自同一色相不同饱和度的方案。这个视觉方案的优点是分类层次一目了然用户可以靠颜色记忆快速定位目标分类。分类详情页顶部的 Tab 用来切换“全部”“初级”“进阶”三个难度筛选。这里有个很容易被忽略的体验问题Flutter 的 TabBar 默认点击会有一个滚动动画动画从当前位置平滑切换到目标位置。在手语学习这种强调快速定位的场景里这个动画反而拖节奏。处理办法是把 TabController 的animationDuration设置为Duration.zero或者干脆用_tabController.index target直接切。TabController( length: 3, vsync: this, animationDuration: Duration.zero, );这样点击后立即切换内容没有中间滚动过程。实测在 OpenHarmony 真机上动画越少列表重建越快体验越干脆。如果你希望保留一点动画给用户反馈建议至少把时间从默认 300ms 降到 80ms 左右小于 100ms 既能感知又不拖沓。4.3 详情跳转与页面栈状态保持点击条目列表中的某一个手语项需要跳到详情页。这里我用的是普通Navigator.push但有个雷点OpenHarmony 分支上如果页面跳转时没有正确配置路由表某些设备上会出现返回后列表位置丢失或者页面空白的情况。我的解决方式是给二级列表加PageStorageKey同时让列表页面 State 混入AutomaticKeepAliveClientMixin。前者会把滚动位置保存到 PageStorage后者让页面在导航栈中被缓存而不是销毁重建。class _CategoryDetailViewState extends StateCategoryDetailView with AutomaticKeepAliveClientMixin { override bool get wantKeepAlive true; }ListView.separated( key: const PageStorageKeyString(category-item-list), ... )状态保持这件事在 Flutter 里经常被忽略但它对手语学习场景特别重要。用户可能在“医疗求助”分类下翻到第 20 个词不小心点进详情看了两秒视频返回的时候如果列表从头开始滚这种挫败感是致命的。5. 适配OpenHarmony会碰到的“特殊战场”5.1 “当前配置的 Flutter SDK 不被完全支持”警告处理这个警告几乎是每个从官方分支切到 OpenHarmony 分支的人第一眼就会看到的The current configured Flutter SDK is not known to be fully supported. Please consult the Flutter documentation for supported SDK versions.看到它先别慌。这不是说你的环境坏了而是 IDE 检查到 Flutter SDK 版本不在某个已知的受支持列表里。OpenHarmony 分支版本号往往和官方主版本不一致IDE 不认很正常。处理方式有两种把工程里pubspec.yaml或 gradle 配置依赖的 Flutter SDK 版本约束修改成当前分支实际报告的版本如果用的是 DevEco 内置检测某些版本下需要关闭对 Flutter SDK 的严格检查或者把 Flutter SDK 的版本配置在项目的local.properties中显式指定。这个警告不影响编译但如果直接忽略后续排查问题时分不清是警告还是真正错误建议工程一建立就处理干净。5.2 Gradle 主插件命令式应用报错与修复工程构建时报过这样一个错误You are applying Flutters main Gradle plugin imperatively using the apply script method, which is deprecated.这个其实不是 OpenHarmony 分支的错而是较新版本 Gradle 对整个 Flutter 插件加载方式的要求变了。旧版 Flutter 工程在android/app/build.gradle或对应模块里用apply from: $flutterRoot/packages/flutter_tools/gradle/app_plugin_loader.gradle这样的命令式加载新版希望用plugins块声明式加载。在 OpenHarmony 工程里同样存在这个问题。修复方法就是把ohos模块的build.gradle中的apply方式迁移到settings.gradle的pluginManagementplugins块plugins { id dev.flutter.flutter-plugin-loader version 1.0.0 id com.android.application version 8.1.0 id org.jetbrains.kotlin.android version 1.9.22 }改完之后同步一次报错消失构建速度还会稍微提升。注意版本号要根据项目实际使用调整不要直接照抄。5.3 打包时 java.lang.AssertionError 问题在打 HAP 包时我撞上了一个挺隐蔽的构建崩溃日志长这样java.lang.AssertionError: java.lang.Exception: Could not close i...被问到完整堆栈里卡在增量缓存相关的类关闭环节。这类问题在即时编译环境下少见但切换分支环境后容易触发主要是历史构建缓存与当前工具链不匹配。解决组合拳flutter clean rm -rf .dart_tool rm -rf ohos/.idea rm -rf ohos/build然后重新flutter pub get再进 DevEco 重新同步。实测这样处理后AssertionError 彻底消失。如果再次出现检查一下磁盘剩余空间和 inode 是否耗尽OpenHarmony 构建会生成大量临时文件空间不足也会导致写入关闭异常。另外注意不要用杀毒软件的实时扫描去盯构建目录Windows 上被文件锁定导致的关闭异常很常见。5.4 Impeller 与 PlatformView 的兼容性取舍渲染引擎方面OpenHarmony 分支逐步在跟进 Flutter 的 Impeller 渲染管线。Impeller 的目标是解决 Skia 在部分 GPU 上的着色器编译掉帧问题但它的设备覆盖范围还在扩展中。手语 App 里有视频播放视频画面必然涉及原生 Surface 和 Flutter 渲染层的叠加如果两者兼容不好就可能出现列表滚动时视频区域白屏、闪烁或者画面撕裂。我这边实际遇到的情况是在部分 OpenHarmony 开发板上开启 Impeller 后卡片列表快速滚动时偶尔掉帧关掉切回 Skia 后反而更稳定。所以在项目里没有强行追求新渲染后端而是通过 Flutter 引擎的FLUTTER_ENGINE_SWITCHES环境变量或者构建参数在 HAP 里显式关闭 Impeller保证视频列表的稳定性。如果你的应用没有 PlatformView 或者视频播放只是普通列表和文字那 Impeller 可以放心开收益是滚动动画更丝滑。就手语学习这种重视频、重列表的场景我建议是“列表和视频尽量分层避免同一个屏幕里出现多个原生 Surface 同时渲染”。详情页的视频全屏播放列表页只放视频封面图这样 PlatformView 的压力会小很多渲染也稳定。6. 实战中遇到的问题与排查技巧实录6.1 列表加载偶发乱序与重复有一次真机上连续切分类再回首页发现分类顺序变化甚至出现重复卡片。排查后发现不是 UI 的问题而是rootBundle.loadString在并发调用时我对同一份数据执行了多次加载并且把结果直接塞进了 Cubit 的状态后一次的加载覆盖前一次中间穿插了状态更新导致列表在 UI 层出现了冗余。解决方式是引入缓存Repository 里缓存第一次加载的结果后续fetchCategories直接返回缓存不重复读资源。同时给 Cubit 的load()加了防重入判断如果状态已经是 loaded 就不重复拉取。class CategoryRepository { ListSignCategory? _cache; FutureListSignCategory fetchCategories() async { if (_cache ! null) return _cache!; final raw await rootBundle.loadString(assets/data/categories.json); final decoded json.decode(raw) as Listdynamic; _cache decoded .map((e) SignCategory.fromJson(e as MapString, dynamic)) .toList(); return _cache!; } }这个坑提醒我数据加载的幂等性在高频操作场景下必须从一开始就考虑。6.2 组件通信与局部刷新分类卡片上的学习进度需要根据用户查看详情的行为实时更新。这个问题本质是组件通信详情页里看完一个视频回到列表页卡片进度要更新。我用的是ValueNotifierAnimatedBuilder组合。每个分类卡片内部持有一个ValueNotifierdouble默认是 0。详情页播放完成后通过回调让页面层拿到对应分类 id然后更新对应的ValueNotifier。卡片只监听自己的 notifier因此只有一个卡片重建不需要整个列表 setState。class _CategoryCard extends StatelessWidget { final SignCategory category; final ValueNotifierdouble progressNotifier; override Widget build(BuildContext context) { return AnimatedBuilder( animation: progressNotifier, builder: (context, child) { return _buildCard(progressNotifier.value); }, ); } }这种方式比 Provider 直接管全量状态更轻量特别适合列表内部小范围动态变化。我到现在都认为不是所有状态都该全局化局部状态用局部刷新是更优解。6.3 跳转原生页面后 Flutter 侧状态丢失项目里有一步操作需要跳转到 OpenHarmony 原生页面比如调起系统分享。这里就碰到 Flutter 跳转原生 Activity 后返回时 Flutter 页面被重建、列表滚动位置丢失的问题。根因是原生页面不在 Flutter 的路由栈里返回时 Flutter view 被重建页面状态没有持久化。处理方式是跳转原生页面之前先用一个NavigatorObserver捕获当前路由信息并把关键状态当前分类 id、滚动位置、过滤条件写进一个全局状态缓存。原生页面返回后在onResume里恢复状态。如果场景允许也可以干脆把分享功能用 Dart 侧的share包实现不路由原生页面这样最省事。6.4 常见问题速查表现象可能原因解决方案列表返回后滚动位置丢失页面被销毁未保存滚动偏移使用 AutomaticKeepAliveClientMixin PageStorageKey分类卡片顺序错乱Repository 并发加载导致状态覆盖缓存首次加载结果load 方法做防重入Tab 切换动画拖节奏默认 animationDuration 过长设Duration.zero或降低到 80ms视频区域白屏Impeller 与 PlatformView 渲染冲突关闭 Impeller分层渲染打 HAP 报 AssertionError构建缓存与工具链不匹配flutter clean 删除缓存目录Gradle 插件格式报错命令式 apply 已废弃迁移到 plugins 声明式加载视频播放无声音或黑屏原生通道未正确实现检查 EventChannel 回调与 AVPlayer 状态机真机安装失败签名配置缺失或 HAP 包名冲突检查签名证书与 module.json5 配置7. 一点实操层面的体会如果让我总结这次分类列表实战最核心的经验我会说三件事。第一模型设计是列表的灵魂。分类、条目、视频路径、颜色、难度这些字段先花半小时想清楚后面省十天工。第二状态管理一定要和数据源解耦。Cubit 不是唯一选择但用 Repository 把数据来源藏住是所有方案里最稳的做法。第三OpenHarmony 适配没有想象中可怕但要预留平台通道的抽象层不要在一个页面里直接调原生方法。分类列表模块跑通之后最爽的瞬间是把同一个 HAP 装上平板和手机看到网格卡片在两种屏幕上自动调整布局而 Dart 代码没有改过一行。这种跨端复用带来的踏实感正是我选择 Flutter 做这个项目的初心。接下来我准备在这个基础上扩展两个方向一个是在分类卡片上叠加每日学习计划的推荐逻辑另一个是把“手语动作相似词”做成一个底部弹出的对比列表。后面有进展了再继续写文分享。
网站建设高端定制企业官网