1. 项目背景与整体设计思路
Flutter + OpenHarmony 的企业级复杂列表布局,我理解是一个“跨平台渲染 + 端侧能力”的工程问题。最近我把一个信息流复杂度很高的业务模块从原生方案迁到了 Flutter 上,然后又跑通了 OpenHarmony 适配,整个过程里最容易翻车的地方不是 UI 还原,而是列表的“状态粒度”和“平台集成”。这篇文章不聊虚的,我直接把从业务建模、状态拆分、分页加载到 OpenHarmony 打包验证的流程写出来。适合三种人看:准备在 OpenHarmony 设备上落地 Flutter 的团队、正在处理长列表卡顿和数据流混乱的 Flutter 开发者、以及想评估 ArkTS 和 Flutter 如何取舍的选型人员。
复杂列表之所以“复杂”,从来不是列表控件本身难写,而是它同时背了三笔账:一是数据量大,不能一次性全塞进内存;二是列表项的类型多,不同卡片有完全不同的交互和渲染逻辑;三是列表状态散落在各个页面、各个 controller 里,一旦滚动起来,状态同步和组件通信就成了性能瓶颈。这些问题在普通小应用里不明显,但规模一上来,垃圾回收、重建频率、平台通道调用开销都会被放大。所以这篇文章的核心不是教你怎么写一个 ListView,而是怎么从架构层面把复杂列表拆成可维护、可测试、可复用的工程模块。
1.1 为什么选 Flutter 而不是 ArkTS
先回答一个社区里高频问题:OpenHarmony 不是有 ArkTS 吗,为什么还要用 Flutter?我的答案是,这取决于你手里有什么团队和代码资产。如果你已经有成熟的 Flutter 跨端业务,那么 OpenHarmony 只是一个新的目标平台,Flutter 的 UI 描述能力和自绘渲染可以让你把原有组件库、状态管理、业务逻辑尽量复用过去。这种情况下,Flutter 是降本增效的选择,而不是技术情怀。
另外一个很现实的原因是 Flutter 的 Impeller 渲染引擎。OpenHarmony 设备谱系很广,从轻量设备到高刷新率平板都有,列表在滚动时最容易暴露渲染引擎的短板。Impeller 通过在运行时预编译着色器,大幅减少了传统 Skia 路径下首帧卡顿和滚动中偶发掉帧的问题。虽然 Impeller 在 OpenHarmony 上的适配成熟度还在演进,但实测下来长列表滚动比纯 Skia 缓存方案稳定不少。这一点对“企业级”场景非常重要,因为用户对信息流卡顿的容忍度极低。
ArkTS 当然有自己的优势:系统 API 调用更直接、IDE 调试链路更短、对 OpenHarmony 新特性的响应速度更快。但它和 Flutter 不是二选一的纯技术竞赛,而是工程资产和团队技能的匹配题。如果一个团队本身没有 Dart/Flutter 积累,又只做 OpenHarmony 单平台,那 ArkTS 是更短的路径;如果已经有跨平台诉求,Flutter 依然是值得投入的方向。
1.2 复杂列表的场景建模
在企业级业务里,“复杂列表”通常长这样:首页信息流里有图文卡片、视频卡片、商品卡片、运营 banner,甚至同一个卡片在埋点、曝光、点击上报上的逻辑都不一样。列表还要支持下拉刷新、上拉加载、滑动删除、置顶、吸顶分类标签,以及进入列表后恢复到上次滚动位置。
我在做需求拆解时,不会直接去写 UI,而是先把列表项的数据结构抽象出来。比如用一个 sealed class 或者带 type 字段的统一模型,区分 article、video、product、ad。这样列表的 itemBuilder 只需要按 type 分发到不同的 Widget,避免在同一个 build 方法里堆大量 if-else。这个设计看起来简单,但很多团队是“先写出来再说”,等产品加第十种卡片时才发现整个 itemBuilder 已经膨胀到上千行,改一个字段都要小心翼翼。
另一个关键建模点是列表的滚动位置。企业级列表往往需要和 Tab 联动,比如首页有顶部 tab,每个 tab 里是一个复杂的无限列表。如果每个 tab 都维护自己的 ScrollController 和 PageStorageKey,内存和状态恢复都会变得很复杂。我建议把每个 tab 的滚动位置保存在一个顶层状态对象里,而不是依赖 Widget 内部的隐式状态。这样即使用户切走 tab 再切回来,也可以用 jumpTo 精确恢复,而不需要重新加载数据。
1.3 选型背后的几个关键问题
在动手之前,我列出过几个决定项目走法的问题:第一,列表数据是服务端下发还是端侧生成?这决定了要不要做缓存和离线包。第二,列表项的 UI 是固定模板还是需要远程动态配置?如果产品要求运营可以配置卡片样式,那就要把 Card Widget 抽象成配置驱动。第三,OpenHarmony 设备上的弱网环境如何处理?复杂列表往往是信息流,图片、视频加载占据了大量资源,必须设计资源分级加载策略。
这些问题在项目启动时不问清楚,后面都会变成“技术债利息”。我见过一个项目,列表卡片由后端动态下发 JSON 渲染,结果为了赶进度,全部用 FutureBuilder 乱拉数据,最后页面重建频繁到点击响应延迟 500ms 以上。所以我们最后确定了一个基础架构:数据层用 Repository 模式,UI 层用 Provider 做状态管理,列表控件用 ListView.builder + Sliver 系列组合,数据加载统一走分页器。这套方案在 Flutter 社区验证得最多,到了 OpenHarmony 平台也最容易排查问题。
2. 核心细节解析与实操要点
2.1 列表控件选型:为什么组合使用 ListView 和 Sliver
很多 Flutter 开发者一上来就用 ListView.builder,这没有错,但企业级复杂列表通常需要多个滚动区域配合,比如顶部有一个可折叠的信息头、中间是搜索栏,下面才是无限列表。这种场景下 ListView.builder 的“全屏滚动”语义会限制布局自由度,更好的选择是 CustomScrollView + SliverList/SliverGrid/SliverAppBar。
我用一个实际例子说明。我们的列表页顶部有一个会吸顶的频道 tab,tab 下方是卡片列表。如果用单个 ListView,需要把 tab 放进 ListView 的 header,然后通过监听滚动偏移做吸顶效果,代码复杂且容易和安卓原生吸顶手势冲突。而用 CustomScrollView 后,tab 区域直接是一个 SliverPersistentHeader,吸顶、折叠、回弹都由 Sliver 机制接管,滚动性能也更好,因为 Sliver 可以延迟构建可视区域之外的节点。
Sliver 的另一个好处是支持组合布局。比如列表的前 3 个位置是运营 banner 区,后面是双列商品瀑布流,再往后是视频卡片流。用一个 CustomScrollView 把 SliverToBoxAdapter、SliverGrid、SliverList 按顺序排列,就能在一个滚动容器里实现混合布局,而不是嵌套多个 ListView。嵌套 ListView 是性能杀手,尤其在 OpenHarmony 设备上,嵌套滚动会导致手势竞争和布局重复计算,能避免就避免。
2.2 多类型 Item 的渲染策略
复杂列表第二个核心是“类型分发”。我推荐的做法是:先定义一个抽象接口 ListItemModel,所有列表项 model 都实现这个接口。然后在 itemBuilder 里根据 runtimeType 或 type 字段来返回对应 Widget。关键点是每个 Widget 都要保持“小而独立”,不要把多个卡片类型混在一个文件里。我们团队按业务域拆分文件,比如 article_card.dart、video_card.dart,每个文件只负责自己的布局和交互。
代码上大概是这样:
abstract class ListItemModel { int get itemType; } class ArticleModel implements ListItemModel { @override int get itemType => 0; final String title; final String coverUrl; // ... } class VideoModel implements ListItemModel { @override int get itemType => 1; // ... }然后 itemBuilder 里这样分发:
Widget _buildItem(BuildContext context, ListItemModel model) { switch (model.itemType) { case 0: return ArticleCard(model: model as ArticleModel); case 1: return VideoCard(model: model as VideoModel); default: return SizedBox.shrink(); } }这里要注意,不要在每个 item 构建时做耗时操作。图片解码、网络请求、JSON 解析都必须在 itemBuilder 外面完成。如果列表项需要异步加载数据,应该只加载单个卡片的数据,而不是在 itemBuilder 里发起每个卡片的网络请求。我们在实际项目里,列表数据是一页一页从 Repository 拿到的,itemBuilder 只做模型到 Widget 的映射,所以滚动起来非常稳定。
2.3 Provider 怎么用:状态拆分和组件通信
热词榜里经常有人搜“flutter provider 怎么用”,说明状态管理依然是新手和老手都会纠结的问题。Provider 本身很简单,难的是怎么设计状态的作用域和更新粒度。复杂列表最容易出现的问题是:一个列表项的状态变化,导致整个列表页面 rebuild,性能瞬间崩掉。
我的做法是三层状态拆分。第一层是页面级状态 PageProvider,管理列表分页数据、刷新状态、错误状态;第二层是局部状态,比如某个列表项的展开/收起、播放状态,这类状态直接用 StatefulWidget 内部管理即可;第三层是跨页面共享状态,比如用户登录信息、全局设置,用顶级 Provider 管理。
在列表场景里,最值得推荐的是 Consumer 和 Selector。Consumer 可以只监听某一个 Provider 的变化,并且我们可以把它包在单个列表项外面,而不是包住整个 ListView。这样某个 item 的局部数据变化时,只有那个 item 重建。区别用代码看得更清楚:
// 坏做法:整个 list 页面监听状态 Consumer<ListState>( builder: (context, listState, _) { return ListView.builder( itemCount: listState.items.length, itemBuilder: (context, index) => ArticleCard(item: listState.items[index]), ); }, ) // 好做法:同一个列表,但每个 item 单独监听需要的状态 ListView.builder( itemCount: itemCount, itemBuilder: (context, index) { return Consumer<ItemState>( builder: (context, itemState, _) { return ArticleCard( item: itemState.items[index], isFolded: itemState.foldedIndex == index, ); }, ); }, )组件通信在复杂列表里也很关键。卡片之间的通信,比如“点击列表里的收藏按钮,要在列表头部显示收藏数”,我建议通过共享 Provider 完成,而不是通过回调层层传递。我们的做法是:定义 FavoritesProvider,列表项按钮在点击时调用 provider.toggleFavorites(model.id),顶部的收藏数量组件通过 Consumer 监听同一个 provider。这样通信链路由“卡片到列表页再到头部”的广播式调度,变成了简单的共享状态订阅,代码可维护性高很多。
2.4 分页加载和错误重试
企业级列表绝对逃不掉分页。分页的坑不在接口本身,而在状态流转。一个健壮的分页器至少要覆盖:首次加载、加载更多、下拉刷新、加载失败、加载成功但数据为空、已经加载到底,这几种状态。很多初学者把分页状态放在 Item 数组里,用 if (items.length == list.length) 判断是否加载到底,这在服务端返回异常时很容易出错。
我建议用一个状态机来管理:
- initial:未加载任何数据
- loadingMore:正在加载下一页
- loadingRefresh:正在刷新
- failed:加载失败,error 信息
- empty:无数据
- done:全部加载完
- data:有数据,正常状态
在列表底部显示什么,完全由这个状态决定。加载中转圈,失败显示“点击重试”,已到底显示“没有更多了”。核心逻辑是每次请求后根据返回数据更新状态,而不是在 build 里临时判断。这样列表的 UI 和请求逻辑彻底解耦,也方便写单元测试。
分页参数上我们也统一处理。服务端如果支持 cursor 分页,就用 cursor 而不是页码,因为信息流新增数据频繁,页码分页容易重复或遗漏。每条数据用 immutable 的 model 保存,列表数据放在 List 中,状态变化时用 copyWith 生成新列表。
3. 实操过程与核心环节实现
3.1 Windows 环境下 Flutter 和 OpenHarmony 开发环境准备
这套技术栈最让人头疼的是环境搭建,很多社区提问“flutter 新建项目后跑不起来”大多发生在这一步。先说 Windows 上 Flutter 的标准安装配置:去 Flutter 官方渠道下载对应 SDK,解压后把bin目录加到 PATH,然后运行flutter doctor检查依赖。如果要做 Windows 桌面端调试,Visual Studio 是必需的,需要安装“使用 C++ 的桌面开发”工作负载。但 OpenHarmony 开发不依赖 Visual Studio,它主要依赖 OpenHarmony SDK 和 DevEco Studio 的命令行工具。
OpenHarmony 侧的基础依赖包括:OpenHarmony SDK(包含 API version 对应的工具链)、hvigor 构建工具、Node.js 环境,以及配置好的ohpm包管理器。Flutter 在 OpenHarmony 上不是直接跑在系统自带的 Dart VM 上的,而是通过 OpenHarmony SIG 提供的适配分支,把 Flutter 引擎编译成 OpenHarmony 可加载的产物。所以我的建议是,先不要把 Flutter 主分支和 OpenHarmony 适配分支混用,而是单独 clone 一个用于 OpenHarmony 的 Flutter SDK,避免版本冲突。
环境配置完以后,推荐用命令行创建一个最小项目跑通设备:flutter create --platforms ohos my_list_app。注意这里的ohos是适配分支新增的平台标识,如果找不到这个参数,说明你的 Flutter SDK 还是标准版,需要切换分支。跑通 hello world 之后再进入复杂列表开发,不然一上来就集成业务代码,环境报错和业务报错混在一起,非常难定位。
3.2 构建集成:理解 Flutter AAR 和 OpenHarmony 工程的配合方式
社区热词“flutter aar”指的是 Flutter 模块被编译成原生工程可依赖的 AAR 产物,这种集成方式在 Android 生态里很成熟,OpenHarmony 的集成思路类似,但产物细节有差异。在实际操作中,我会先把 Flutter 业务模块做成一个独立的 library 工程,然后通过构建工具把它打包成 OpenHarmony 原生侧可以引用的产物,再在主应用工程里配置依赖。
这里的重点是依赖关系和版本对应。Flutter 引擎、Dart SDK、OpenHarmony SDK 的版本必须锁死,否则会出现运行时符号找不到或插件注册失败。我建议在工程根目录放一个versions.json,记录所有关键版本号,CI 构建时统一从这里读取。这个文件也用于团队新成员快速初始化环境,避免在群里反复对版本号。
集成过程中最容易报错的一条是:
You are applying Flutter's main Gradle plugin imperatively using the apply method, which is not supported. Please use the plugins {} DSL instead.这个报错在 OpenHarmony 工程接入 Flutter 模块时经常出现。原因是 Flutter 的 Gradle 插件默认使用apply plugin的方式,而较新的 Gradle 工程要求在plugins {}块中声明插件。解决方法有两个:要么把工程里的 Flutter 相关依赖改为使用 plugins DSL 方式,要么把 Gradle 版本降到 Flutter 插件适配的版本。我一般建议前者,因为升级 Gradle 才是长期方向,临时降版本只会让其他依赖跟着变老。
3.3 实现一个可复用的复杂列表 Demo
下面给出一个可以直接套用的核心代码结构。假设我们的列表有图片卡片、视频卡片、运营 banner 三种类型,顶部有一个吸顶标签,列表支持分页加载。整个页面用一个 CustomScrollView 承载,状态由 ListState 管理。
class ComplexListPage extends StatelessWidget { @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (_) => ListState(), child: Consumer<ListState>( builder: (context, state, _) { return CustomScrollView( controller: state.scrollController, slivers: [ SliverAppBar( title: Text('企业级复杂列表'), pinned: true, expandedHeight: 120, ), SliverPersistentHeader( pinned: true, delegate: ChannelTabHeaderDelegate( tabs: state.tabs, currentIndex: state.currentIndex, ), ), state.isLoading ? SliverToBoxAdapter(child: Center(child: CircularProgressIndicator())) : SliverList.builder( itemCount: state.items.length + 1, itemBuilder: (context, index) { if (index == state.items.length) { return _buildLoadMoreFooter(state); } return _buildItem(context, state.items[index]); }, ), ], ); }, ), ); } }这里的_buildLoadMoreFooter会根据 state 的分页状态显示加载中、加载失败重试、已经到底三种 UI。state.items里每新增一页数据,都会在 Consumer 触发 rebuild 后更新列表。因为 SliverList.builder 只在可视区域构建 item,所以即使数据增加,也不会一次性创建所有 Widget。
关于 item 本身,还有一个容易被忽略的优化:给每个列表项的根 Widget 加上const构造,或者至少让它在没有数据变化时保持相同引用。我经常看到有人忘记这件事,导致 itemBuilder 里创建新 Widget,引发 setState 后整个列表重建。企业级列表一定要用AutomaticKeepAliveClientMixin谨慎控制 keepAlive,只在真正需要保持状态的 item 上使用,不要全局开启。
3.4 数据层和 UI 层如何对接
列表页的基本流程是:进入页面后触发 state.loadFirstPage(),Repository 从服务端拉取数据,返回模型列表。这里有一个设计细节:Repository 返回的数据先要转换为ListItemModel,再放入状态列表。不要在 UI 层直接解析 JSON,也不要在 model 里保留服务端字段。
我们用一个通用的PagedResult<T>类包装返回值:
class PagedResult<T> { final List<T> items; final String? nextCursor; final bool hasMore; final String? error; const PagedResult({required this.items, this.nextCursor, required this.hasMore, this.error}); }ListState 里的 load 方法逻辑是:
- 请求前先设置 loadingMore / loadingRefresh 状态
- 请求成功后判断 hasMore,更新 items 和 nextCursor
- 请求失败时设置 failed 状态,保留原有 items,不丢失已加载内容
- 无论成功失败,最后通知 listeners 触发 UI 更新
这正是 Provider 最舒服的用法:状态对象只负责维护数据,UI 只负责监听和渲染。你在组件通信时可以随时调用 provider 的方法,而不用考虑 setState 作用域的问题。
4. 性能优化与问题排查实录
4.1 Impeller 对长列表滚动的影响
Flutter 2.x 时期,社区讨论最多的就是 Skia 的着色器编译抖动。首次滚动列表中未缓存的着色器会导致掉帧,在复杂列表里尤其明显。Impeller 在 OpenHarmony 的适配进展让这个问题的改善幅度很大,但并不意味着所有性能问题都消失了。
实测中,我发现在 OpenHarmony 设备上开启 Impeller 后,列表首帧构建时间略有增加,但滚动帧率更稳定。原因是 Impeller 在运行时提前编译了必要的 GPU 管线。如果你的列表运行在低端设备上,建议先用性能分析工具跑一遍,看是不是 GPU 瓶颈。如果是普通文本和图片列表,瓶颈往往在 Dart 对象创建和图片解码,渲染引擎反而影响不大。
在复杂列表里,最值得关注的性能收益其实是减少平台通道调用。Flutter 和 OpenHarmony 原生交互需要通过 channel 通信,如果每个列表项都去查询系统状态(比如网络类型、定位、电池电量),通道开销会非常吓人。我自己踩过的坑是:列表页每显示一个卡片就调用一次原生方法获取设备屏幕宽度,结果列表滚动时页面直接掉到 40 帧。解决方案是启动时一次性读取设备信息,然后通过顶层 Provider 分发给所有列表项。
4.2 Flutter 新建项目后跑不起来的常见原因
热词搜索里高频出现“flutter新建项目后跑不起来”,这大概率是环境问题而不是代码问题。OpenHarmony 场景下我遇到的四大原因如下。第一,Flutter 适配分支和 OpenHarmony SDK 版本不匹配,导致 flutter create 之后无法识别 ohos 平台。第二,OpenHarmony SDK 环境变量没有正确配置,构建工具找不到 SDK 路径。第三,Hvigor 和 Node.js 版本不对,编译到一半报错。第四,设备连接后没有授权 USB 调试,或者设备系统版本过低。
解决办法很简单:重新跑一遍flutter doctor -v,仔细看每个检查项是否通过。但 doctor 有时候不会报告 OpenHarmony 专门的配置,建议你打开环境变量文件确认OHOS_SDK_HOME、DEVECO_SDK_HOME之类的路径。我们团队还在项目根目录写了一个setup_env.bat,一键设置所有环境变量,新人工机直接双击跑完就能开发。
另一个容易踩坑的是本地之前装过标准 Flutter,后来又切换 OpenHarmony 适配分支,结果flutter config里的缓存互相干扰。我建议调试 OpenHarmony 设备时,专门用一个独立的用户目录来存放适配分支的缓存,避免和标准 Flutter 混用。因为两个分支的 engine artifact 版本不一样,混用时经常出现“莫名其妙跑不起来”的问题。
4.3 常见运行时崩溃和 Gradle 集成报错
运行时崩溃最经典的一种是启动时日志打出:
E/flutter (31173): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: MissingPluginException ...这个报错在 Flutter 标准平台上是某个插件没有注册,到了 OpenHarmony 场景更是高发。原因是 OpenHarmony 的插件注册机制和 Android 不完全一样,很多 Flutter 插件需要有一个 OpenHarmony 原生侧的实现,如果没有实现对应方法,Dart 侧一调用就会抛出 MissingPluginException。排查思路是:先定位是哪个插件的方法调用失败,然后检查 OpenHarmony 工程是否引入了对应的原生端实现。
还有一类崩溃和 Gradle 集成有关。报错You are applying Flutter's main Gradle plugin imperatively using the apply method,几乎可以肯定是在settings.gradle或build.gradle里没有用 plugins DSL 声明 Flutter 插件。解决办法是把:
apply plugin: 'com.flutter.gradle'改成:
plugins { id 'com.flutter.gradle' version '...' }然后同步一次 Gradle。如果项目是 hvigor 工程,还要看 Flutter 插件是否提供了对应的 hvigor 插件适配。这个报错本质上不是 Flutter 的问题,而是构建工具链和插件加载约定的演进,遇到后不要慌,先确认插件版本。
4.4 OpenHarmony XTS 认证对列表应用有什么要求
热词里的“openharmony xts 认证”,指的是 OpenHarmony 兼容性测试套件。它和普通单元测试不同,主要用来验证应用和设备是否符合 OpenHarmony 的兼容性规范。如果我们要把应用部署到通过认证的设备上,或者我们希望自己的设备/应用获得兼容性认证,就必须跑 XTS 套件。
对复杂列表应用来说,XTS 主要关注的是稳定性、API 兼容性、资源使用规范。我们之前为了过 XTS,把列表页里一些非标准调用全部整改了。最典型的是不要绕过系统 API 直接操作文件路径,不要在列表滚动时频繁申请权限,不要把私有目录硬编码。还有一个点是应用退出时,所有 Flutter Engine 资源必须正确释放,否则 XTS 的内存检测会报警。这些要求其实反过来帮助我们写出更健壮的代码。
跑 XTS 时,我建议把列表页作为重点模块压测两轮:一轮是快速滚动到列表底部再回到顶部,一轮是中途反复切换网络。XTS 对 ANR 和卡顿检测比较严格,如果列表在主线程做了 JSON 解析,大概率会超时。所以复杂列表的所有数据解析都必须在 isolate 里做,或者提前在 Repository 层转换成模型对象。
4.5 问题排查速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| flutter 新建项目后跑不起来 | Flutter 版本/OpenHarmony SDK 不匹配,环境变量配置错误 | 用flutter doctor -v查看,并使用独立缓存分支 |
| 列表滚动掉帧 | 图片解码耗时、状态更新粒度过大、Impeller 未开启 | 使用图片缓存插件,拆细 Provider 监听,确认 Impeller 配置 |
| 启动即 MissingPluginException | OpenHarmony 侧插件没有实现 | 检查插件是否支持 OpenHarmony,注册原生实现 |
| 点击列表项响应延迟 | 主线程执行了 JSON 解析或数据库查询 | 把耗时操作移到 Isolate/子线程 |
| Gradle 集成报 apply 方法错误 | 构建脚本声方式不兼容 | 改用 plugins DSL 声明并升级 plugin 版本 |
| 列表滚动后内存暴增 | 列表项没有释放图片资源、KeepAlive 过多 | 用 DevTools 检查 image cache,控制 KeepAlive 范围 |
| XTS 认证失败 | 内存泄漏、权限使用不规范、未释放 Engine | 跑 LeakCanary/内存分析,清理原生调用 |
这张表总结的都是我在 OpenHarmony 复杂列表项目中真实遇到过的问题。列表项目最怕的不是某个单点 bug,而是所有问题交织在一起。所以我习惯在项目初期就把日志框架和性能监控埋好,不然出了问题只能盲猜。
5. 几条值得记住的工程化经验
5.1 关于组件通信和状态管理的取舍
使用 Provider 做复杂列表的状态管理,最重要的经验不是怎么调用context.read和context.watch,而是什么时候不该用 Provider。列表项的局部 UI 状态,比如展开、选中、动画进度,如果都放进全局 Provider,整个列表的监听范围就会无限膨胀。正确做法是局部状态留在 StatefulWidget,只有跨卡片、跨页面、影响多个组件的数据才提升到 Provider。
组件通信方面,我的建议是“优先共享状态,其次回调,最后才是 EventBus”。回调在层级少的时候够用,但列表卡片往往嵌了好几层:卡片里有按钮,按钮里可能又弹出一个底部面板。如果都用回调,人肉传递至少四五层,改一个参数要联动好多文件。我们后来把所有业务事件统一收口到 Provider 方法里,点击收藏、点击跳转、曝光上报都通过 provider 转发,测试也更好做。
5.2 不要在列表项里做太重的事
这条经验看起来像废话,但我每次复盘都会提到。列表项里尽量不要出现:HttpClient直接请求、File读取、jsonDecode、MediaQuery.of(context).size频繁调用。这些操作要么是异步导致 item 重建后状态错乱,要么是同步导致掉帧。企业级列表应该是一个“展示模型”的纯 UI 层,所有数据准备都在前面完成。
我在代码评审时有一条默认规则:如果一个 itemBuilder 超过了 40 行,或者里面出现了 async 方法调用,就说明拆分不够细。正确做法是把 card 拆成ArticleCard、ArticleCardActions、ArticleCardFooter,每个组件只做一件事。这样即使某个卡片需要特殊交互,也不会影响同一列表里的其他卡片。
5.3 后续可以继续扩展的方向
如果列表规模继续增长,下一步可以做三件事。第一,把分页器封装成通用组件,配合 OpenHarmony 的弱网测试工具验证断点续传。第二,引入 Fish-Redux 或者 Riverpod 来替代手写 Provider,主要看团队熟悉度和状态复杂程度。第三,加上列表性能监控面板,在 debug 模式下实时展示帧率、内存和图片缓存命中情况。这些扩展不会改变文章里提到的基础架构,只是让工程化程度更高。
我个人在实际操作中的体会是:Flutter × OpenHarmony 的复杂列表并没有秘诀,无非是把“状态、渲染、数据”三层拆干净,然后接受平台差异带来的摩擦。环境配置、Gradle/hvigor 集成、XTS 测试这些事确实磨人,但每一步都是值得的,因为一旦跑通,这套列表方案不仅能跑 OpenHarmony,未来接其他平台也会很快。如果大家在自己的项目里遇到类似的坑,按文章里的排查表一条条过,大部分问题都能解决。