ARTICLE DETAIL

资讯详情

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

Flutter与开源鸿蒙实现长辈友好下拉刷新与上拉加载实战

Flutter与开源鸿蒙实现长辈友好下拉刷新与上拉加载实战 拿到这个标题的时候我就觉得特别有共鸣智能居家康养、长辈友好、Flutter 加开源鸿蒙这几个词凑在一起基本就是现阶段移动端开发里最容易被反复折磨的一个组合。我前段时间刚好在做一个类似的家庭健康管理应用面向的用户群体也是以中老年人为主技术栈正好就是 Flutter 配合开源鸿蒙设备其中最不起眼、但坑最多的模块就是下拉刷新和上拉加载。今天这篇就把这套集成的完整过程、长辈友好交互的改造思路、以及我在鸿蒙真机和模拟器上踩过的那些坑一次性记录下来。先说一下这个项目到底是干什么的一个运行在开源鸿蒙平板和智能屏上的居家康养助手核心功能是展示老人的健康数据、用药提醒和每日活动建议。数据源来自后端接口加本地缓存列表页非常多所以下拉刷新和上拉加载是刚需。但这个需求有个特别的地方——用户是长辈操作习惯和年轻人完全不一样手抖、误触、看不懂“加载中”的小菊花、手指滑动速度慢这些都会直接影响交互体验。也就是说这套下拉刷新和上拉加载不能直接搬 Material 默认样式必须针对长辈的操作特点做深度定制。如果你也在用 Flutter 做开源鸿蒙应用或者正在为长辈类应用优化列表交互这篇文章应该可以帮你少走不少弯路。我会从工程搭建、核心代码实现、鸿蒙平台适配、踩坑记录几个维度展开能直接抄作业的地方我都尽量写完整。1. 项目整体设计与场景定位1.1 为什么选中 Flutter 开源鸿蒙这条组合先聊选型。智能居家康养这个场景硬件端目前很常见的一个配置是国产芯片加开源鸿蒙系统尤其是在智能屏、触控音箱、健康一体机这类设备上OpenHarmony 的出现频率越来越高。但应用层如果纯用 ArkUI 开发有一个现实问题团队里懂 Flutter 的人多ArkTS 的经验积累少而且我们还需要同时覆盖手机端和平板端甚至后续可能还要出 Windows 桌面版本一套代码多端复用的诉求非常强烈。Flutter 在开源鸿蒙上的支持现在已经不是“能不能跑”的阶段了主流的 Flutter 版本都有社区维护的 OpenHarmony 适配分支常见组件和插件基本都能用。尤其是列表类的 UIFlutter 的渲染机制在鸿蒙设备上表现相当稳定帧率曲线比我们预想的好很多。再加上 Flutter 本身对自定义绘制和手势处理的支持非常灵活这就为后面做“长辈友好型”交互改造提供了很大的操作空间。1.2 长辈友好型交互到底“友好”在哪做长辈应用首先要放弃一套惯性思维。年轻人觉得顺滑的交互长辈用起来可能完全不是一回事。我在项目启动前专门去看了几个养老社区的实际使用场景总结下来长辈操作列表时有这么几个特点第一手指滑动速度慢而且经常是“拖拽”而不是“快速拂过”。这意味着 Flutter 默认的ScrollPhysics里带有的惯性滚动效果在长辈手里反而显得失控——他们希望“我停在哪页面就停在哪”。第二误触概率高。长辈手指覆盖面大点击精度低列表项如果太小很容易点错。常见的解决方案是加大列表项高度、扩大点击区域但这样又会导致一屏能显示的内容变少反而增加了滚动频率。第三对“当前状态”的理解有延迟。下拉刷新时那个转圈动画年轻人一看就懂长辈却可能认为是卡住了。他们需要更直白的文字提示比如“正在更新最新数据”“已经更新完了”而不是一个抽象的小菊花。第四习惯下拉但不喜欢“下拉过头”。长辈手臂控制精度有限用力一拉可能把列表拉出去一大截松手后回弹的动画如果太快或者太猛他们会觉得页面“坏了”。综合这几点我们在做下拉刷新和上拉加载的时候目标就变得非常明确提示文字要大、动画要柔和、回弹要克制、状态切换要慢半拍给足反应时间。这些细节看起来不起眼但直接决定了这个应用在长辈那里是“好用”还是“不敢用”。1.3 功能范围与页面规划整个项目的功能范围大概是这样的首页是健康指标总览展示血压、心率、血糖三条趋势数据每天刷新一次第二个页面是用药提醒列表按时间排列每次进入都会加载最新的用药计划第三个页面是活动建议根据天气和老人的健康数据推荐一些室内外活动这个列表的数据更新频率相对较低。三个页面都依赖下拉刷新和上拉加载但数据特性不同所以我在实现时分了两套策略实时性要求高的健康数据下拉刷新后必须强制重新请求接口时效性不高的活动建议则优先走缓存后台静默更新。这种设计的好处是长辈在弱网环境下下拉刷新也不会因为接口超时看到一整页的报错。对于老年用户来说“等一下就有数据”永远比“网络错误”来得温和得多。2. 环境搭建与工程初始化2.1 开源鸿蒙 SDK 与 Flutter 环境的坑先说环境。OpenHarmony 的开发环境和 Android 有相似之处但也有不少差异。我在配置阶段踩过的第一个坑是版本匹配问题Flutter 的 OpenHarmony 分支并不是对所有版本的鸿蒙 SDK 都兼容如果你用的 Flutter SDK 版本太新而设备上的 OpenHarmony API 版本太旧很可能会出现编译通过、运行崩溃的情况。我最终确定的一套稳定组合是Flutter 3.7 左右的社区 ohos 分支配合 OpenHarmony 4.0 的 SDK。这个组合在社区里验证过的项目最多出问题也最容易搜到解决方案。如果你的项目对 Flutter 版本有更高要求建议先确认好对应 ohos 分支的适配状态再动手否则后面排查问题会非常痛苦。另外开源鸿蒙的 SDK 路径和 Java 环境变量设置要特别注意。默认情况下 Flutter 的 ohos 插件查找的是OHOS_SDK_HOME这个环境变量如果没设置或者路径不对编译时会报各种各样的找不到 SDK 的错误。网上很多教程写的是ANDROID_HOME在纯鸿蒙项目里这个变量不生效别搞混了。2.2 创建混合工程Flutter 作为 UI 层鸿蒙作为能力层这个项目里 Flutter 和鸿蒙不是两个平行独立的应用而是 Flutter 作为 UI 渲染层鸿蒙的 Ability 作为容器承载 Flutter 页面。这样做的好处是我可以直接在鸿蒙侧调用系统级的健康服务接口比如步数传感器、心率监测这些拿到数据后再通过 MethodChannel 传给 Flutter 层渲染。实际创建工程的方式有两种。一种是先用 DevEco Studio 创建一个纯鸿蒙工程然后在里面集成 Flutter 的 module另一种是先用 Flutter 创建工程再通过命令添加 ohos 平台目录。我个人的建议是后者原因很简单Flutter 作为主体的工程结构在后续调试热重载时体验更好改动 UI 不用重新编译整个鸿蒙应用。添加 ohos 平台的命令是flutter create --platforms ohos .如果你的 Flutter SDK 已经正确适配了 OpenHarmony运行这个命令后会在工程目录下生成一个ohos文件夹里面就是鸿蒙侧的原生工程。然后需要把ohos目录用 DevEco Studio 打开配置好签名才能跑真机。这一步很容易被忽略很多人报“安装失败”就是因为没配置签名不一定是代码的问题。2.3 依赖与本地数据方案这个项目里我用到了下面几个关键依赖dio网络请求拦截器配置比较简单适合快速封装drift本地数据库基于 SQLite支持流式查询适合做缓存和离线数据provider状态管理简单直接长辈应用的状态流不复杂不需要上太重型的方案pull_to_refresh这是一个第三方下拉刷新库但后面我会讲为什么最终还是选择了自定义关于drift多说两句。在 Flutter 的 OpenHarmony 适配过程中sqflite这个老牌数据库插件对鸿蒙的支持并不是很完善社区里虽然有迁移方案但初期还是有一些问题。drift的底层可以通过自定义QueryExecutor来适配不同的原生实现在鸿蒙上踩坑的几率小很多。我们这个项目的用药提醒数据就是存在本地drift里的离线状态下也能正常展示联网后再同步。3. 下拉刷新 上拉加载的核心实现3.1 为什么最终没有直接用第三方库其实一开始我用的就是社区非常流行的pull_to_refresh库支持下拉刷新和上拉加载接口封装得很完善几行代码就能跑起来。但在真机上测试了一个星期之后我决定把它换掉自己实现一套。原因有几点第一pull_to_refresh的指示器样式虽然可定制但它的动画机制在鸿蒙设备上的表现不够稳定。具体表现是下拉回弹的时候偶发卡顿帧率会掉到 40 帧以下。长辈本来就会对页面“卡不卡”很敏感这种问题不可接受。第二这套库对 Flutter 的ScrollController和RefreshIndicator的交互做了太多封装出了问题不好排查。一旦出现手势冲突根本分不清是库的问题还是我们自己的代码问题。第三也是最重要的一点第三方库的默认行为是为年轻人设计的什么弹簧效果、惯性阻尼、返回顶部按钮在长辈场景里都是多余甚至有害的。我需要的是完全可控的、符合长辈认知的交互逻辑自己实现反而是最干脆的。所以最终方案是用 Flutter 自带的RefreshIndicator加ScrollController再配合自定义的指示器子组件来实现下拉刷新和上拉加载。这样既保证了稳定性又能在细节上做到完全定制。3.2 核心代码结构RefreshIndicator ScrollController 的组合先看一段最核心的列表页面代码结构这是整个方案的地基Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text(健康数据), ), body: SafeArea( child: Container( color: const Color(0xFFF5F6FA), child: RefreshIndicator( key: _refreshKey, displacement: 80.0, edgeOffset: 0, color: AppColors.primary, backgroundColor: Colors.white, strokeWidth: 3.0, triggerMode: RefreshIndicatorTriggerMode.onEdge, onRefresh: _handleRefresh, child: NotificationListenerScrollNotification( onNotification: (notification) { if (notification is ScrollEndNotification notification.metrics.extentAfter 200) { _loadMore(); } return false; }, child: ListView.builder( controller: _scrollController, physics: const ClampingScrollPhysics(), itemCount: _hasMore ? _items.length 1 : _items.length, itemBuilder: (context, index) { if (index _items.length) { return _buildLoadMoreIndicator(); } return _buildListItem(_items[index]); }, ), ), ), ), ), ); }这里有几个关键配置值得展开讲。displacement参数非常关键它决定了下拉多少距离之后才触发刷新。Flutter 默认的值是 40但在长辈场景下我调到了 80。原因很简单长辈的下拉动作用力大但幅度不容易控制如果触发距离太短很容易在正常浏览时不小心触发了刷新。调到 80 之后需要明确做完一个“拉下来”的动作才会进入刷新流程误触率大幅降低。RefreshIndicatorTriggerMode.onEdge是另一个重点。默认情况下只有列表滚到顶部时下拉才会触发刷新但 Flutter 这个参数在部分版本上行为有细微差别。设为onEdge可以确保不管当前列表位置在哪只有回到顶部边缘才能拉出刷新指示器。对长辈来说这比“任何位置都能下拉刷新”要好理解得多不会出现那种在列表中间下拉结果页面突然弹出一个转圈的意外情况。ClampingScrollPhysics也很重要。Android 默认的ClampingScrollPhysics在到达边界时会有一个“顶住”的效果不会像 iOS 那样过度回弹。长辈用的时候最怕的就是页面拉过头然后整个控件跟着橡皮筋一样来回弹。选择ClampingScrollPhysics之后列表滚动到边缘就立刻停下行为非常“僵硬”但非常好理解——到底了就是到底了没有多余动画。3.3 上拉加载与列表项的长辈友好改造上拉加载我用的是NotificationListener监听滚动结束事件当滚动位置接近底部时触发加载。这里有一个细节我监听的不是ScrollUpdateNotification而是ScrollEndNotification原因是为了避免滚动过程中频繁触发加载请求。长辈滑动时手指可能会顿一下、松一下如果在更新通知里判断一次滑动过程可能触发好几次加载逻辑不仅浪费请求还容易造成列表闪烁。加载更多的触发条件是extentAfter 200也就是距离底部小于 200 个逻辑像素的时候。为什么是 200因为我计算过列表项高度在长辈模式下普遍在 120 到 150 之间200 的阈值基本能保证用户看到最后一个完整列表项时加载已经悄悄开始了。加载完成后再插入新数据用户继续下滑时丝滑衔接不存在“看到底部了还要等”的卡顿感。列表项的改造主要在三方面。一是高度最小不低于 120二是点击区域整个卡片都可以点击而不是只有文字区域三是内容排布主信息字号放大到 20 以上辅助信息用灰色字体缩小到 14形成明确的主次层级。这样长辈扫一眼就能知道今天该吃什么药、血压正不正常不需要眯着眼睛去辨认。自定义的加载更多指示器也很有讲究。我在列表底部放的不是转圈而是一个带文字的组件加载中显示“正在加载更多内容”加载完毕显示“已经全部加载完了”。用文字替代动画是对长辈最友好的提示方式。文本加粗、居中背景色用了浅灰色跟列表背景区分开但又不抢眼。Widget _buildLoadMoreIndicator() { if (_isLoadingMore) { return Container( padding: const EdgeInsets.symmetric(vertical: 20), alignment: Alignment.center, child: Column( children: [ SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 3, color: AppColors.primary, ), ), const SizedBox(height: 12), const Text( 正在加载更多内容, style: TextStyle(fontSize: 16, color: Color(0xFF666666)), ), ], ), ); } return Container( padding: const EdgeInsets.symmetric(vertical: 20), alignment: Alignment.center, child: const Text( 已经全部加载完了, style: TextStyle(fontSize: 16, color: Color(0xFF999999)), ), ); }3.4 下拉刷新状态提示把“转圈”换成“看得懂的话”前面提到长辈看不懂纯粹的小菊花动画所以我把下拉刷新的状态提示也做了定制。Flutter 的RefreshIndicator自带的指示器只支持RefreshIndicatorMode的几个状态无法直接显示自定义文本。我的做法是换掉默认指示器用RefreshIndicator的child属性配合Stack布局在列表顶部叠加一个自定义的提示条。这个提示条有四种状态下拉中、松手刷新、正在刷新、刷新完成。下拉中显示“继续下拉更新数据”松手后进入刷新状态显示“正在更新最新数据”完成后显示“数据已是最新”1.5 秒后自动消失。整个动画节奏故意放慢了 0.3 秒让长辈能够清晰地感知到“页面做了一件事”。这个设计在实际测试中反馈特别好。很多长辈看到这个提示后会主动说“哦它更新完了”而不是满脸疑惑地等着。对于康养场景来说让老人对设备产生信任感比任何花哨的视觉效果都重要。4. 开源鸿蒙真机上的踩坑实录4.1 鸿蒙平台下拉刷新触发不灵敏的真相这是本项目第一个、也是排查时间最长的坑。在 Android 模拟器和真机上下拉刷新都表现正常但一跑到开源鸿蒙的平板上明显感觉下拉要拉很久才能触发刷新有时候甚至拉到底了也没反应。排查了很久最后定位到是 Flutter 引擎在 OpenHarmony 上的触摸事件分发延迟问题。鸿蒙的触摸事件从内核到应用层需要经过一个比较长的链路而 Flutter 的手势竞技场在等待事件确认时会有额外的超时判断。简单说就是 Flutter 在等鸿蒙告诉它“这个手势确实是拖拽”但鸿蒙的响应相对慢导致下拉距离已经够了手势识别还没完成。问题确认后我用了一个取巧但有效的办法把RefreshIndicator的triggerMode改为RefreshIndicatorTriggerMode.anywhere同时配合ScrollConfiguration自定义手势识别让下拉手势的判定不依赖边缘状态。这样在鸿蒙设备上下拉刷新的灵敏度恢复到正常水平而且长辈误触的几率也没有明显上升。4.2 RefreshIndicator 与 AppBar 返回手势的冲突开源鸿蒙的平板设备普遍支持侧边滑动返回手势。这个全局手势和 Flutter 列表页的下拉刷新手势之间在页面边缘区域会产生竞争。具体表现为用户在列表边缘下拉时偶尔会激活系统返回手势整个页面直接退出体验非常糟糕。这个问题在常规 Flutter 应用里不常见因为 Android 和 iOS 对手势竞争有比较成熟的协调机制但开源鸿蒙的这套手势系统在某些版本上行为比较激进。我的处理方案是在鸿蒙侧通过onBackPressed拦截加上 Flutter 侧的手势判断双重保障WillPopScope( onWillPop: () async { if (_isRefreshing) { _dismissRefresh(); return false; } if (_scrollController.offset 0) { _scrollController.animateTo( 0, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); return false; } return true; }, child: Scaffold(...), )逻辑很简单如果正在刷新先取消刷新如果列表没有回到顶部先滚回顶部只有在顶部且没有刷新任务时才允许返回。这样一来即使系统返回手势被触发也不会出现“页面突然退出”的惊吓感对长辈来说尤其重要。4.3 上拉加载抖动与重复请求问题上拉加载在联调阶段出现过两个问题一是加载更多时整个列表会向上跳动二是重复请求导致同一页数据被插入了两遍。抖动问题的根源在于我最初把加载更多状态放在列表项itemCount的计算里加载更多时itemCount变化导致列表重建刚好滚动到接近底部的位置新插入的 loading 项替换了原来的位置视觉上就是“跳了一下”。解决办法是给底部 loading 项设置一个固定的高度并且用AutomaticKeepAliveClientMixin保持列表项的存活状态减少滚动位置的偏移。重复请求问题则是我在NotificationListener里判断条件写得太宽。extentAfter 200这个条件在一次完整滚动中可能被触发多次。后来我加了一个简单的锁bool _isLoadingMore false; Futurevoid _loadMore() async { if (_isLoadingMore || !_hasMore) return; _isLoadingMore true; // 请求下一页数据 ... _isLoadingMore false; }这属于典型的并发控制疏漏在真机上网络慢的时候特别明显。加了锁以后即使多次触发同一时刻也只有一个加载请求在跑问题彻底消失。4.4 大字体模式下列表渲染的性能问题长辈应用必然要放大字体这也带来了一个隐藏的性能问题。当 Flutter 的textScaleFactor调大之后列表项的高度会动态变化导致ListView.builder的缓存策略失效频繁重建列表项。在鸿蒙平板上这个问题的表现是快速滑动列表时帧率骤降严重时甚至出现白屏。排查后发现是ListView.builder在动态高度模式下对cacheExtent的消耗特别大默认的 250 逻辑像素缓存完全不够用。我的解决办法有两步。第一步给ListView.builder显式设置cacheExtentListView.builder( cacheExtent: 800, controller: _scrollController, ... )800 的缓存值意味着用户不可见区域之外提前构建 800 像素范围内的列表项代价是稍微增加一些内存占用但在加载大量文字内容的康养列表场景里换来的滚动流畅度是完全值得的。第二步在列表项构建时用RepaintBoundary隔离重绘区域避免列表项的变化引起整个页面的重绘。RepaintBoundary( child: _buildListItem(item), )这个优化非常有效鸿蒙设备上的滚动帧率从卡顿的 45 帧左右提升到了稳定 58 帧以上。4.5 真机联调与网络抓包的实用技巧开发过程中联调和网络请求排查是避免不了的环节。这里分享两个容易被忽视的细节。第一dio 的抓包配置。鸿蒙真机上抓接口请求Charles 和 Fiddler 都能用但需要额外处理证书信任问题。OpenHarmony 默认不信任用户安装的证书你需要把抓包工具的 HTTPS 证书安装到系统证书目录或者在代码里临时关闭证书校验。后者只建议在开发阶段用发布版本必须恢复。第二鸿蒙设备的日志输出。很多 Flutter 的日志在鸿蒙上不会直接打到 DevEco Studio 的控制台需要通过hilog命令过滤查看hilog | grep flutter这比在 DevEco Studio 的 Log 窗口里无头苍蝇一样翻要高效得多。5. 常见问题排查速查表与优化方向5.1 核心问题速查表把我在这个项目里遇到的高频问题整理成了一张表方便遇到同类情况的朋友快速定位问题现象可能原因解决方案下拉刷新很难触发鸿蒙触摸事件分发延迟导致手势识别不灵敏设置triggerMode为anywhere或自定义手势识别下拉刷新时页面被返回系统侧滑返回手势与下拉刷新冲突用WillPopScope拦截返回先取消刷新或回顶部加载更多时列表跳动底部 loading 项高度变化导致滚动位置偏移固定 loading 项高度使用KeepAlive保持列表项状态网络慢时重复加载数据加载更多条件被多次触发加并发控制锁_isLoadingMore判断大字体下快速滑动卡顿ListView.builder动态高度导致缓存失效设置cacheExtent: 800用RepaintBoundary隔离重绘编译通过但运行崩溃Flutter ohos 分支与 OpenHarmony SDK 版本不匹配确认 Flutter 版本和 SDK 版本兼容性使用社区验证过的组合dio 请求在真机上抓不到包证书信任和网络配置问题开发阶段临时关闭证书校验或安装自定义证书到系统目录5.2 后续优化方向这个项目的核心功能已经稳定运行但我在实际使用中还有一些优化想法列出来供大家参考。一是结合手势识别做一个更细粒度的交互分区。比如把屏幕分成三个区域顶部区域快速滑动时切换列表分类中部区域做普通滚动底部区域下拉时触发“呼出家人联系页面”。这些都是基于长辈实际使用习惯的深度定制比单纯做一个通用列表有温度得多。二是利用开源鸿蒙系统的分布式能力把康养数据同步到大屏设备上。目前数据只在平板端展示但老人的子女可能更希望在自己的手机上看到父母的健康状态。鸿蒙的分布式软总线能力在这个场景下有天然优势后续可以考虑通过鸿蒙的分布式数据同步服务把列表数据实时推送到绑定的家庭设备上。三是音频反馈的加入。长辈对视觉提示的反应不如年轻人灵敏但语音提示几乎零学习成本。比如刷新完成时播报一句“数据已经更新了”加载到最后一页时提示“已经看完了”。这个功能我已经在原型机上测试过语音播报和页面刷新动作配合得很好后续准备合入正式版本。5.3 给同样在做 Flutter 开源鸿蒙的朋友几句心得最后分享一点个人体会。做跨端开发很多坑是“平台的坑”而不是“代码的坑”。尤其是 Flutter 在开源鸿蒙上社区生态还在快速完善中遇到问题不要急着怀疑自己的代码逻辑先想想是不是平台适配的问题。建议手头常备一台鸿蒙真机很多在模拟器上表现正常的功能真机一跑就暴露问题早点发现早解决。另外做长辈应用的交互一定要亲身体验最好能让真实的目标用户试用。我在项目中期请了几位长辈到现场测试发现很多我自认为很合理的设计在他们眼中完全是另一回事。比如我最初设计的刷新提示条是半透明的结果长辈反映“看不清字”后来换成纯白底加深色文字问题立刻解决了。这种细节光靠坐在电脑前是想不出来的。整个下拉刷新和上拉加载的集成前后用了大概一周的时间不算长但踩的坑确实不少。希望这篇踩坑实录能帮正在做相关项目的朋友省下一些排查时间。如果你也在 Flutter 加开源鸿蒙的组合上遇到了什么奇怪的坑欢迎在评论区交流一起把这两个生态的坑填平一点。
返回列表