flutter三方库适配鸿蒙(flutter_refresh)实战+下拉刷新
Flutter纯Dart实现的flutter_refresh库可无缝适配鸿蒙系统,因其不含原生代码,仅依赖应用项目的平台目录结构。该库核心包含:1)支持下拉/上拉回调的Refresh组件;2)统一滚动物理效果确保跨平台体验;3)五状态刷新控制器管理生命周期;4)可自定义的默认刷新指示器。适配时只需创建带ohos/目录的Flutter项目,配置鸿蒙签名后引入库即可。关键在于理解库作为"菜谱
前言
最近在做 Flutter 鸿蒙适配的时候,遇到一个很常见的需求:下拉刷新。翻了一圈发现 flutter_refresh 这个库挺合适的,关键是它是纯 Dart 实现,理论上不需要任何适配就能跑在鸿蒙上。
实际测试下来确实如此,这篇文章就记录一下整个过程,踩过的坑也一并分享。

一、先搞清楚一个问题:为什么这个库能直接跑在鸿蒙上?
在开始之前,我觉得有必要先聊聊这个问题,因为很多人可能会疑惑:一个没有 ohos/ 目录的库,怎么就能在鸿蒙上运行了?
答案其实很简单:库和应用是两码事。
我们来看一下 flutter_refresh 的目录结构:
flutter_refresh-master/
├── lib/
│ └── src/
│ ├── refresh.dart
│ ├── refresh_child.dart
│ ├── refresh_controller.dart
│ └── refresh_widget.dart
├── example/
│ ├── ios/
│ └── lib/
└── pubspec.yaml
注意看,lib/ 目录下全是 .dart 文件,没有任何原生代码。这意味着什么?
意味着这个库只是提供了一堆 Dart 代码,至于这些代码最终跑在 iOS 还是鸿蒙上,完全取决于你的应用有没有对应的平台目录。
打个比方:库就像一本菜谱,只告诉你怎么做菜;而 ios/、ohos/ 这些目录就像是不同的厨房。菜谱本身不需要厨房,但你要做菜,就得有个厨房。
所以当你的应用项目里有 ohos/ 目录时,Flutter 会把库的 Dart 代码编译进去,然后在鸿蒙的"厨房"里运行。
二、看看库的核心实现
在用一个库之前,我习惯先翻翻它的源码,了解一下大概的实现思路。这样遇到问题的时候心里有底。
2.1 Refresh 组件的定义
打开 lib/src/refresh.dart,可以看到 Refresh 组件的定义:
class Refresh extends StatefulWidget {
final RefresherCallback? onHeaderRefresh;
final RefresherCallback? onFooterRefresh;
final RefreshController? controller;
final ScrollController? scrollController;
final RefreshScrollViewBuilder? childBuilder;
final ScrollPhysics? physics;
final Widget? child;
这里定义了几个关键属性。onHeaderRefresh 就是下拉刷新的回调,onFooterRefresh 是上拉加载的回调。两个都是可选的,你可以只用其中一个,也可以两个都用。
childBuilder 和 child 二选一,用来指定要包裹的滚动组件。childBuilder 更灵活一些,因为它会把 controller 和 physics 传给你,让你自己决定怎么用。
2.2 滚动物理效果的处理
库里有一段代码挺有意思的:
static ScrollPhysics createScrollPhysics(ScrollPhysics? src) {
ScrollPhysics physics = const AlwaysScrollableScrollPhysics()
.applyTo(const BouncingScrollPhysics());
if (src != null) {
return physics.applyTo(src);
}
return physics;
}
这段代码的作用是创建一个统一的滚动物理效果。BouncingScrollPhysics 是 iOS 风格的弹性滚动,AlwaysScrollableScrollPhysics 确保即使内容不足一屏也能滚动。
为什么要这样做?因为下拉刷新需要用户能够"过度滚动"(overscroll),也就是滚动到顶部之后还能继续往下拉。如果没有这个物理效果,用户拉不动,自然也就触发不了刷新。
这个设计让库在 iOS、鸿蒙上都能有一致的体验,不会因为平台默认的滚动行为不同而出问题。
2.3 刷新状态的管理
再看看 refresh_controller.dart:
enum RefreshState { drag, ready, loading, complete, error }
class RefreshWidgetController extends ChangeNotifier {
double _value = 0;
RefreshState _state = RefreshState.drag;
库定义了 5 种刷新状态:
drag:用户正在拖动,还没到触发刷新的阈值ready:拖动距离够了,松手就会刷新loading:正在刷新中complete:刷新完成error:刷新出错
这个状态机的设计让刷新过程的每个阶段都可控。比如你可以根据不同状态显示不同的提示文字:“下拉刷新” → “松开刷新” → “刷新中…” → “刷新完成”。
2.4 默认的刷新指示器
refresh_child.dart 里定义了默认的刷新指示器:
class DefaultRefreshChild extends StatefulWidget {
final RefreshWidgetController controller;
final Widget icon;
final bool up;
final bool showLastUpdate;
这个组件会根据刷新状态显示不同的 UI。icon 是那个箭头图标,up 控制箭头方向(下拉刷新朝下,上拉加载朝上),showLastUpdate 控制是否显示上次刷新时间。
如果你觉得默认的样式不好看,完全可以自己写一个替换掉。
三、项目准备
3.1 创建一个支持鸿蒙的 Flutter 项目
如果你的项目是新建的,用下面这个命令可以直接创建带鸿蒙支持的项目:
flutter create --platforms=ohos my_app
这里 --platforms 参数指定了要支持的平台,ohos 就是鸿蒙。执行完之后,项目里会自动生成 ohos/ 目录。
如果是老项目想加鸿蒙支持,也简单,在项目根目录执行:
flutter create --platforms=ohos .
注意最后那个点,表示在当前目录操作。执行完就会多出一个 ohos/ 目录,里面的结构大概是这样的:
ohos/
├── AppScope/
│ ├── app.json5
│ └── resources/
│ └── base/
│ ├── element/
│ │ └── string.json
│ └── media/
│ └── app_icon.png
├── entry/
│ ├── src/
│ │ └── main/
│ │ ├── ets/
│ │ │ ├── entryability/
│ │ │ │ └── EntryAbility.ets
│ │ │ └── pages/
│ │ │ └── Index.ets
│ │ ├── resources/
│ │ └── module.json5
│ ├── build-profile.json5
│ └── oh-package.json5
├── build-profile.json5
├── hvigorfile.ts
└── oh-package.json5
这个结构是鸿蒙应用的标准结构。简单解释一下几个关键目录:
-
AppScope/:应用级别的配置,包括应用名称、图标等。app.json5里定义了 bundleName(包名)、版本号等信息。 -
entry/:应用的入口模块。鸿蒙应用可以有多个模块,但至少要有一个 entry 模块。 -
entry/src/main/ets/:这里放的是 ArkTS 代码。EntryAbility.ets是应用的入口 Ability,Index.ets是首页。Flutter 鸿蒙版会在这里加载 Flutter 引擎。 -
entry/src/main/module.json5:模块配置文件,定义了模块的能力(Ability)、权限等。 -
build-profile.json5:构建配置,指定签名、编译选项等。 -
oh-package.json5:依赖管理文件,类似 npm 的 package.json。
3.2 鸿蒙签名配置
鸿蒙应用需要签名才能安装到真机上。打开 ohos/build-profile.json5,可以看到签名相关的配置:
{
"app": {
"signingConfigs": [
{
"name": "default",
"type": "HarmonyOS",
"material": {
"certpath": "",
"storePassword": "",
"keyAlias": "",
"keyPassword": "",
"profile": "",
"signAlg": "SHA256withECDSA",
"storeFile": ""
}
}
],
"products": [
{
"name": "default",
"signingConfig": "default"
}
]
}
}
开发阶段可以用自动签名,在 DevEco Studio 里配置一下就行。如果是命令行开发,需要手动配置证书路径。
3.3 引入 flutter_refresh
打开 pubspec.yaml,在 dependencies 下面加上:
dependencies:
flutter:
sdk: flutter
flutter_refresh:
path: ../flutter_refresh-master
这里我用的是本地路径引用,因为我是直接下载的源码。如果你是从 pub 上装的,写法会不一样,不过原理是一样的。
加完之后执行一下:
flutter pub get
看到 Got dependencies! 就说明依赖拉取成功了。
四、开始写代码
4.1 创建主入口
先把 main.dart 的基本结构搭起来:
import 'package:flutter/material.dart';
import 'package:flutter_refresh/flutter_refresh.dart';
void main() {
runApp(const MyApp());
}
这两行 import 分别引入了 Flutter 的 Material 组件库和 flutter_refresh 库。runApp 是 Flutter 应用的入口,传入根组件启动应用。
4.2 创建应用根组件
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Refresh Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const RefreshDemo(),
);
}
}
MaterialApp 是 Material Design 风格应用的根组件。debugShowCheckedModeBanner: false 去掉右上角那个碍眼的 DEBUG 标签。useMaterial3: true 启用 Material 3 设计语言,界面会更现代一些。
4.3 创建演示页面
class RefreshDemo extends StatefulWidget {
const RefreshDemo({super.key});
State<RefreshDemo> createState() => _RefreshDemoState();
}
因为下拉刷新涉及到数据变化,所以用 StatefulWidget。如果用 StatelessWidget,数据变了 UI 也不会更新。
4.4 定义状态变量
class _RefreshDemoState extends State<RefreshDemo> {
int _itemCount = 10;
_itemCount 用来控制列表显示多少条数据。下拉刷新的时候,我们会重置这个值,模拟"刷新"的效果。
变量名前面的下划线 _ 表示这是一个私有变量,只能在当前文件内访问。这是 Dart 的命名约定。
实际项目中,这里可能是一个 List<T>,存放从接口拉回来的数据模型。
4.5 构建列表项
Widget _itemBuilder(BuildContext context, int index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text('Item $index'),
subtitle: const Text('下拉刷新 / 上拉加载更多'),
);
}
这个方法负责构建每一行的 UI。ListView.builder 会调用它来生成列表项,index 是当前项的索引。
ListTile 是 Material Design 的列表项组件,自带 leading(左侧图标)、title(标题)、subtitle(副标题)等插槽,用起来很方便。
CircleAvatar 是圆形头像组件,这里用来显示序号。${index + 1} 是字符串插值,把数字转成字符串。加 1 是因为索引从 0 开始,但显示给用户看的序号一般从 1 开始。
4.6 实现下拉刷新的回调
重点来了,这是下拉刷新的核心逻辑:
Future<void> onHeaderRefresh() {
return Future.delayed(const Duration(seconds: 2), () {
setState(() {
_itemCount = 10;
});
});
}
几个关键点:
返回值必须是 Future。库内部会等这个 Future 完成才收起刷新动画。看一下库的源码就知道为什么:
Future<void> loading(ScrollMetrics metrics) {
changeState(RefreshState.loading);
dynamic result = callback();
// ...
result.whenComplete(() {
changeState(RefreshState.drag);
});
return result;
}
库调用你的回调之后,会用 whenComplete 监听 Future 完成,然后才把状态改回 drag。如果你返回的不是 Future,whenComplete 会立刻执行,动画瞬间就结束了。
Future.delayed 是模拟网络请求。实际项目中,这里应该是调接口的代码:
Future<void> onHeaderRefresh() async {
try {
final response = await api.fetchList(page: 1);
setState(() {
_dataList = response.data;
});
} catch (e) {
// 处理错误,比如显示 Toast
print('刷新失败: $e');
}
}
setState 触发 UI 更新。这是 Flutter 状态管理的基础。调用 setState 之后,Flutter 会重新执行 build 方法,用新的数据渲染 UI。
4.7 组装 Refresh 组件
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Refresh 示例'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: SafeArea(
child: Refresh(
onHeaderRefresh: onHeaderRefresh,
childBuilder: (BuildContext context,
{ScrollController? controller, ScrollPhysics? physics}) {
return ListView.builder(
controller: controller,
physics: physics,
itemBuilder: _itemBuilder,
itemCount: _itemCount,
);
},
),
),
);
}
Scaffold 是 Material Design 的页面脚手架,提供了 appBar、body、floatingActionButton 等常用布局插槽。
SafeArea 很重要,它能避免内容被刘海屏、底部导航栏、状态栏等遮挡。鸿蒙设备的屏幕形态比较多样,有的有刘海,有的有挖孔,加上这个保险一点。
childBuilder 的参数里,controller 和 physics 必须传给子列表。这是个容易踩的坑。
为什么?因为 Refresh 组件需要监听滚动事件来判断用户是不是在下拉。它通过 controller 来控制滚动位置,通过 physics 来实现弹性滚动效果。如果你不传这两个参数,它就监听不到,下拉刷新自然也就不生效了。
我第一次用的时候就忘了传,结果怎么拉都没反应,排查了半天才发现是这个问题。
五、运行到鸿蒙设备
代码写完了,接下来就是见证奇迹的时刻。
5.1 检查设备连接
先确认鸿蒙设备已经连上电脑,执行:
flutter devices
如果看到类似这样的输出,说明设备已经识别到了:
FMR0223825079397 (mobile) • FMR0223825079397 • ohos-arm64 • Ohos OpenHarmony-6.0.1.120 (API 21)
如果显示 Unauthorized,需要在设备上点一下"允许 USB 调试"的弹窗。有时候弹窗会被其他应用挡住,可以下拉通知栏看看。
如果设备完全没识别到,检查一下:
- USB 线是不是数据线(有些线只能充电)
- 设备的开发者选项是不是打开了
- USB 调试是不是开启了
- hdc 工具是否正常工作(可以执行
hdc list targets检查)
5.2 运行项目
flutter run -v
-d 后面跟的是设备 ID,就是上一步看到的那个。
第一次运行会比较慢,因为要编译鸿蒙的 HAP 包。控制台会显示编译进度:
Launching lib/main.dart on FMR0223825079397 in debug mode...
start hap build...
Running Hvigor task assembleHap...
Hvigor 是鸿蒙的构建工具,类似 Gradle。它会编译 ArkTS 代码、打包资源、生成 HAP 文件。
耐心等一会儿,看到下面这样的输出就说明成功了:
✓ Built ohos/entry/build/default/outputs/default/entry-default-signed.hap.
installing hap. bundleName: com.example.newqwer
Syncing files to device FMR0223825079397...
HAP 是鸿蒙应用的安装包格式,类似 iOS 的 IPA。entry-default-signed.hap 表示这是 entry 模块的 debug 签名包。
这时候应用已经装到设备上并启动了。试试下拉,应该能看到刷新动画,2 秒后数据会"刷新"(虽然我们只是重置了数量,但效果是一样的)。
5.3 热重载
运行起来之后,修改代码保存,按 r 键可以热重载,改动会立刻生效,不用重新编译。这个功能在调试 UI 的时候特别好用。
如果热重载不生效(比如改了 State 的初始化逻辑),按 R 键热重启,会重新初始化整个应用。
5.4 查看日志
调试的时候经常需要看日志。Flutter 的 print 输出会显示在控制台里。如果想看更详细的鸿蒙系统日志,可以用 hdc 命令:
hdc hilog
这个命令会实时输出设备的系统日志,包括 Flutter 引擎的日志、崩溃信息等。
六、遇到的一些问题
6.1 Dart 版本兼容问题
如果库的 pubspec.yaml 里 SDK 约束太老,可能会报错:
This requires the 'super-parameters' language feature to be enabled.
这是因为代码里用了 super.key 这种新语法,但 SDK 约束还是老的。
解决办法是把库的 SDK 约束改成:
environment:
sdk: ">=2.17.0 <4.0.0"
然后重新 flutter pub get。
6.2 鸿蒙签名问题
如果运行时报签名错误,检查一下 ohos/build-profile.json5 里的签名配置是否正确。开发阶段建议用 DevEco Studio 配置自动签名,会省很多事。
6.3 设备连接不稳定
有时候设备会突然断开连接,重新插拔 USB 线通常能解决。如果还是不行,试试重启 hdc 服务:
hdc kill
hdc start
七、小结
整体来说,flutter_refresh 适配鸿蒙的过程还是比较顺利的,主要得益于它是纯 Dart 实现。
如果你也在做 Flutter 鸿蒙适配,选库的时候可以优先考虑纯 Dart 的库,能省不少事。那些带原生代码的库(比如有 ios/ 目录的),就需要额外写鸿蒙的原生实现了,工作量会大很多。
下一篇会继续讲上拉加载的实现,其实和下拉刷新差不多,换个回调就行。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐




所有评论(0)