OpenHarmony中Flutter实战:实现流畅的下拉刷新与上拉加载效果
前言
随着鸿蒙生态的飞速发展,OpenHarmony跨平台开发成为了开发者关注的热点方向。Flutter作为Google推出的优秀跨平台UI框架,其鸿蒙适配版本让开发者能够借助熟悉的Dart语言,高效构建HarmonyOS应用,大幅降低了跨平台开发的学习成本与开发难度。
在移动应用的开发场景中,列表的下拉刷新和上拉加载是两大高频且核心的交互功能。无论是新闻资讯类、社交类应用,还是图片浏览类应用,这两个功能都是提升用户体验的关键:下拉刷新能让用户快速获取最新的内容,上拉加载则通过分页加载的方式减少单次请求的数据量,避免因数据过大导致的页面卡顿,让列表滚动更丝滑。
本教程将从零开始,带领大家开发一个具备完整下拉刷新和上拉加载功能的风景图库应用,深入讲解Flutter鸿蒙版在实现这类核心功能时的技术选型、架构设计与代码实现,让开发者掌握在OpenHarmony平台下构建流畅列表交互的核心技巧。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


开发背景与项目目标
开发背景
OpenHarmony生态的持续完善,推动了跨平台开发技术在鸿蒙体系中的落地。Flutter凭借其跨端一致性、高性能渲染和丰富的组件生态,成为鸿蒙跨平台开发的重要选择。而列表刷新加载作为移动应用的基础能力,其实现的流畅度、稳定性直接影响用户的使用体验,也是开发者在鸿蒙Flutter开发中需要重点攻克的基础问题。
项目目标
本项目将打造一个功能完整的风景图库应用,实现以下核心功能目标,且所有功能均在DevEco Studio模拟器中验证通过:
| 功能点 | 具体要求 |
|---|---|
| 下拉刷新 | 支持连续多次刷新操作,刷新过程无卡顿、无延迟 |
| 上拉加载 | 自动检测列表滚动位置,智能触发分页加载,加载过程不影响滚动 |
| 状态提示 | 完善的状态反馈,包含加载中、加载失败、无更多数据等提示 |
| 鸿蒙适配 | 完美适配OpenHarmony平台,在DevEco Studio模拟器中正常运行 |
技术选型
本项目选用的技术栈均为鸿蒙Flutter开发中的主流技术,兼顾了性能、易用性和生态成熟度,具体选型及核心作用如下:
| 技术 | 版本 | 核心作用 |
|---|---|---|
| Flutter | 3.27.4 鸿蒙版 | 跨平台UI框架,实现一次开发多端适配 |
| Dart | 3.x | Flutter的开发语言,完成业务逻辑与UI编写 |
| pull_to_refresh | ^2.0.0 | 下拉刷新核心组件,提供丰富的刷新样式与交互 |
| infinite_scroll_pagination | ^4.0.0 | 分页管理核心组件,自动化处理无限滚动分页逻辑 |
| DevEco Studio | 6.0.0 | 鸿蒙专属开发IDE,完成应用的开发、调试与模拟器验证 |
| Picsum API | - | 免费开源的图片数据源,提供高质量风景图片,无需注册 |
技术方案选型
核心组件对比与选择
在实现下拉刷新和上拉加载功能时,我们选用了Flutter生态中两款成熟的开源组件,各司其职实现功能,兼顾开发效率与运行性能。
1. pull_to_refresh
作为Flutter生态中最流行的下拉刷新组件之一,是实现本项目下拉刷新功能的核心选择,在pubspec.yaml中的依赖配置如下:
yaml
dependencies:
pull_to_refresh: ^2.0.0
核心优势:
- 提供经典、苹果、水滴等多种刷新样式,满足不同UI设计需求;
- 支持横向和纵向两种刷新方式,适配多场景布局;
- 可与ListView、GridView等Flutter原生滚动组件无缝集成,无兼容问题;
- 底层优化良好,性能优异,刷新操作不会影响列表的滚动流畅度。
2. infinite_scroll_pagination
专门针对无限滚动场景设计的分页组件,完美解决上拉加载的分页管理问题,依赖配置如下:
yaml
dependencies:
infinite_scroll_pagination: ^4.0.0
核心优势:
- 自动化管理分页状态,无需手动维护页码、数据列表等状态;
- 内置完善的错误处理和重试机制,降低开发成本;
- 支持ListView、GridView等各种滚动视图,适配性强;
- 状态管理逻辑简洁清晰,有效减少业务代码的冗余。
数据源设计
本应用选择Picsum Photos API作为图片数据源,该API是Flutter开发中常用的免费图片接口,选择理由如下:
- 完全免费,无需注册和申请密钥,开箱即用;
- 支持根据参数生成随机图片,轻松实现分页不同的图片内容;
- 返回的图片为高质量风景照片,契合本项目风景图库的定位;
- API响应稳定、速度快,避免因数据源问题导致的页面卡顿。
项目架构设计
为保证代码的可维护性、可扩展性和可读性,本项目采用经典的分层架构模式,同时搭配合理的状态管理策略,让业务逻辑与UI展示解耦,便于后续功能迭代与问题排查。
整体架构
项目的代码目录结构清晰,按功能划分为不同层级,各层级职责单一,通过固定的数据流进行交互,具体结构如下:
lib/
├── api/ # API服务层:封装数据请求的业务逻辑
│ └── image_api.dart # 图片数据接口,处理图片列表的请求
├── core/ # 核心配置层:存放全局常量、配置信息
│ └── http/
│ └── api_config.dart # API配置常量,管理基础地址、分页大小等
├── models/ # 数据模型层:定义实体类,完成数据解析与封装
│ └── image_model.dart # 图片实体模型,对应接口返回数据结构
├── pages/ # 页面层:存放应用的所有页面
│ └── image_list_page.dart # 列表主页面,实现刷新加载核心交互
├── widgets/ # 组件层:封装可复用的自定义UI组件
│ └── image_card.dart # 图片卡片组件,展示单张图片及信息
└── main.dart # 应用入口:初始化应用,配置根页面
核心数据流说明 :
用户触发刷新/加载操作 → pull_to_refresh/infinite_scroll_pagination组件响应 → 调用API服务层的请求方法 → 从Picsum API获取数据 → 数据模型层解析数据 → 更新分页控制器状态 → PagedListView自动刷新UI,展示数据。
状态管理策略
本项目采用局部状态管理方案,结合组件自身的状态控制器,实现下拉刷新和上拉加载的状态管理,核心设计是将两个功能的状态分离,避免状态冲突。核心状态变量定义如下:
dart
// 核心状态变量
class ImageListPageState {
// 下拉刷新控制器,管理刷新状态
final RefreshController _refreshController;
// 分页控制器,管理上拉加载的分页状态
final PagingController<int, ImageModel> _pagingController;
// 独立的加载状态(关键设计!)
bool _isRefreshing = false; // 仅标记下拉刷新状态
bool _isLoadingMore = false; // 仅标记上拉加载状态
// 每页加载的数据量
final int _pageSize = 6;
}
核心设计要点:
- 下拉刷新和上拉加载使用独立的状态变量,互不干扰;
- 从根源上避免因共用状态导致的刷新卡住、操作被阻止等问题;
- 在
finally代码块中确保状态的正确重置,防止异常导致的状态错乱。
核心功能实现
本项目的核心功能实现遵循分层架构的原则,从依赖配置开始,逐步完成API配置、数据模型、API服务、UI组件和页面的开发,层层递进,确保代码的规范性。
一、依赖配置
首先在项目根目录的pubspec.yaml文件中添加所需的第三方依赖,包括下拉刷新、分页组件和Flutter核心SDK:
yaml
dependencies:
flutter:
sdk: flutter
# 下拉刷新组件
pull_to_refresh: ^2.0.0
# 无限滚动分页组件
infinite_scroll_pagination: ^4.0.0
添加完成后,执行以下命令安装依赖:
bash
flutter pub get
二、API配置层
创建lib/core/http/api_config.dart文件,封装全局的API配置常量,实现配置的统一管理,便于后续修改:
dart
/// API 配置类,管理全局API相关常量
class ApiConfig {
/// Picsum Photos API 基础地址
static const String imageBaseUrl = 'https://picsum.photos';
/// 每页加载的数据量,全局统一
static const int pageSize = 6;
/// 网络请求延迟(模拟真实网络环境,便于调试)
static const int networkDelay = 500;
}
三、数据模型层
创建lib/models/image_model.dart文件,定义图片实体模型ImageModel,完成从JSON数据到实体对象的解析,以及实体对象到JSON的转换,实现数据的封装:
dart
/// 图片数据模型,对应Picsum API返回的数据结构
class ImageModel {
final String id; // 图片唯一标识
final String url; // 图片网络URL地址
final int width; // 图片原始宽度
final int height; // 图片原始高度
// 构造方法,强制传入所有必选参数
ImageModel({
required this.id,
required this.url,
required this.width,
required this.height,
});
/// 从JSON数据创建ImageModel对象,处理空值避免崩溃
factory ImageModel.fromJson(Map<String, dynamic> json) {
return ImageModel(
id: json['id']?.toString() ?? '',
url: json['url']?.toString() ?? '',
width: json['width'] as int? ?? 0,
height: json['height'] as int? ?? 0,
);
}
/// 将ImageModel对象转换为JSON,便于后续数据存储/传输
Map<String, dynamic> toJson() => {
'id': id,
'url': url,
'width': width,
'height': height,
};
}
四、API服务层
创建lib/api/image_api.dart文件,封装图片数据的请求逻辑,作为业务层与数据源的中间层,对外提供统一的调用方法:
dart
import '../models/image_model.dart';
import '../core/http/api_config.dart';
/// 图片API服务类,封装图片列表的请求逻辑
class ImageApi {
/// 获取风景图片列表
/// [page] 页码(从1开始)
/// [pageSize] 每页数量
/// 返回解析后的图片模型列表
static Future<List<ImageModel>> getImageList({
required int page,
required int pageSize,
}) async {
// 模拟真实网络延迟,贴合实际开发场景
await Future.delayed(Duration(milliseconds: ApiConfig.networkDelay));
// 根据页码生成偏移量,确保每页返回不同的图片数据
final offset = (page - 1) * pageSize;
// 模拟API返回数据,生成图片模型列表
return List.generate(pageSize, (index) {
final randomNum = offset + index + 1;
return ImageModel(
id: 'pic$randomNum',
url: '${ApiConfig.imageBaseUrl}/800/600?random=$randomNum',
width: 800,
height: 600,
);
});
}
}
五、UI组件层
UI组件层分为图片卡片组件和列表主页面,组件化开发让UI代码可复用、易维护,同时实现核心的交互功能。
5.1 图片卡片组件
创建lib/widgets/image_card.dart文件,封装ImageCard自定义组件,负责单张图片的展示、加载失败占位和信息展示,是列表的核心子组件:
dart
import 'package:flutter/material.dart';
import '../models/image_model.dart';
/// 图片卡片组件,展示单张图片及相关信息
class ImageCard extends StatelessWidget {
final ImageModel image; // 接收图片模型数据
const ImageCard({super.key, required this.image});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2, // 轻微阴影,提升UI层次感
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildImageWidget(), // 构建图片展示区域
_buildImageInfo(), // 构建图片信息展示区域
],
),
);
}
/// 构建图片展示组件,处理加载失败场景
Widget _buildImageWidget() {
return ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: Image.network(
image.url,
width: double.infinity,
height: 200,
fit: BoxFit.cover, // 等比例缩放,填充容器
// 图片加载失败时的占位组件
errorBuilder: (context, error, stackTrace) {
return Container(
height: 200,
color: Colors.grey[300],
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.broken_image, size: 40, color: Colors.grey),
SizedBox(height: 8),
Text('图片加载失败,请重试', style: TextStyle(color: Colors.grey)),
],
),
),
);
},
),
);
}
/// 构建图片信息展示组件,显示图片ID和尺寸
Widget _buildImageInfo() {
return Padding(
padding: const EdgeInsets.all(12),
child: Text(
'图片ID: ${image.id} | 尺寸: ${image.width} × ${image.height}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
);
}
}

5.2 列表主页面
创建lib/pages/image_list_page.dart文件,实现应用的核心页面ImageListPage,整合下拉刷新、上拉加载组件,完成核心交互逻辑,是整个应用的功能核心:
dart
import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import '../api/image_api.dart';
import '../models/image_model.dart';
import '../widgets/image_card.dart';
import '../core/http/api_config.dart';
/// 风景图库列表主页面,实现下拉刷新和上拉加载核心功能
class ImageListPage extends StatefulWidget {
const ImageListPage({super.key});
@override
State<ImageListPage> createState() => _ImageListPageState();
}
class _ImageListPageState extends State<ImageListPage> {
// 下拉刷新控制器,管理刷新的开始、完成、失败状态
final RefreshController _refreshController = RefreshController();
// 分页控制器,初始化第一页页码为1
final PagingController<int, ImageModel> _pagingController =
PagingController(firstPageKey: 1);
// 每页数据量,引用全局配置
final int _pageSize = ApiConfig.pageSize;
// 独立的状态标记,避免刷新和加载冲突(核心设计)
bool _isRefreshing = false; // 下拉刷新状态
bool _isLoadingMore = false; // 上拉加载状态
@override
void initState() {
super.initState();
// 绑定分页请求监听器,触发上拉时自动调用加载方法
_pagingController.addPageRequestListener(_fetchPageData);
// 应用初始化时加载第一页数据
_loadFirstPage();
}
@override
void dispose() {
// 页面销毁时释放控制器资源,避免内存泄漏
_refreshController.dispose();
_pagingController.dispose();
super.dispose();
}
/// 加载第一页数据,应用初始化时调用
Future<void> _loadFirstPage() async {
try {
final imageList = await ImageApi.getImageList(page: 1, pageSize: _pageSize);
if (mounted) {
// 判断是否为最后一页(数据为空或不足一页)
final isLastPage = imageList.isEmpty || imageList.length < _pageSize;
if (isLastPage) {
_pagingController.appendLastPage(imageList);
} else {
// 加载下一页的页码为2
_pagingController.appendPage(imageList, 2);
}
}
} catch (e) {
if (mounted) {
// 加载失败时标记分页控制器错误状态
_pagingController.error = e;
}
}
}
/// 下拉刷新逻辑处理
Future<void> _onRefresh() async {
// 若已有刷新/加载操作,直接结束刷新,避免冲突
if (_isRefreshing || _isLoadingMore) {
_refreshController.refreshCompleted();
return;
}
_isRefreshing = true;
try {
// 刷新时重新加载第一页数据
final imageList = await ImageApi.getImageList(page: 1, pageSize: _pageSize);
if (mounted) {
final isLastPage = imageList.isEmpty || imageList.length < _pageSize;
// 直接更新分页状态,替换原有数据(刷新核心操作)
_pagingController.value = PagingState(
itemList: imageList,
nextPageKey: isLastPage ? null : 2,
);
}
// 刷新成功,结束刷新状态
_refreshController.refreshCompleted();
} catch (e) {
if (mounted) {
// 刷新失败,显示提示信息
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('刷新失败:$e'), backgroundColor: Colors.red),
);
}
// 标记刷新失败
_refreshController.refreshFailed();
} finally {
// 无论成功失败,重置刷新状态(必写)
_isRefreshing = false;
}
}
/// 上拉加载更多数据,接收分页控制器传递的页码
Future<void> _fetchPageData(int pageKey) async {
// 若已有刷新/加载操作,直接返回,避免冲突
if (_isRefreshing || _isLoadingMore) return;
_isLoadingMore = true;
try {
final imageList = await ImageApi.getImageList(page: pageKey, pageSize: _pageSize);
if (mounted) {
final isLastPage = imageList.isEmpty || imageList.length < _pageSize;
if (isLastPage) {
_pagingController.appendLastPage(imageList);
} else {
// 加载下一页,页码自增
_pagingController.appendPage(imageList, pageKey + 1);
}
}
} catch (e) {
if (mounted) {
_pagingController.error = e;
}
} finally {
// 无论成功失败,重置加载状态(必写)
_isLoadingMore = false;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('风景图库'),
centerTitle: true,
elevation: 1,
),
// 下拉刷新外层组件
body: SmartRefresher(
controller: _refreshController,
onRefresh: _onRefresh, // 绑定下拉刷新回调
header: const ClassicHeader(), // 经典刷新样式
// 分页列表组件,作为刷新组件的子组件
child: PagedListView<int, ImageModel>(
pagingController: _pagingController,
// 列表项构建代理,配置各类状态UI
builderDelegate: PagedChildBuilderDelegate<ImageModel>(
// 构建列表项,复用图片卡片组件
itemBuilder: (context, item, index) => ImageCard(image: item),
// 上拉加载中提示UI
newPageProgressIndicatorBuilder: (_) => const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(strokeWidth: 2),
),
),
// 无更多数据提示UI
noMoreItemsIndicatorBuilder: (_) => const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('已加载全部风景图片', style: TextStyle(color: Colors.grey)),
),
),
// 首页加载失败提示UI,提供重试按钮
firstPageErrorIndicatorBuilder: (_) => Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
const SizedBox(height: 16),
const Text(
'首页数据加载失败',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
const SizedBox(height: 8),
const Text('请检查网络后重试', style: TextStyle(color: Colors.grey)),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _loadFirstPage,
child: const Text('重新加载'),
),
],
),
),
),
),
),
),
);
}
}
六、核心代码解析
本项目实现流畅刷新加载的关键在于合理的状态管理 和分页控制器的正确使用,以下是核心设计点的解析:
6.1 状态分离设计
这是解决刷新和加载冲突的核心设计,也是本项目的关键亮点:
dart
// ❌ 错误做法:共用一个状态变量,易导致操作冲突、页面卡住
bool _isLoading = false;
// ✅ 正确做法:分离为两个独立的状态变量,各司其职
bool _isRefreshing = false; // 仅标记下拉刷新状态
bool _isLoadingMore = false; // 仅标记上拉加载状态
状态分离的优势:
| 场景 | 共用状态问题 | 独立状态优势 |
|---|---|---|
| 下拉刷新中触发上拉 | 状态冲突,上拉操作被错误阻止 | 各自独立,上拉操作可正常触发(或合理拦截) |
| 上拉加载中触发下拉 | 状态冲突,刷新操作卡住,无法结束 | 并行处理,刷新操作可正常执行,体验更丝滑 |
| 请求异常退出 | 状态未正确重置,后续操作全部失效 | 可单独重置对应状态,不影响另一功能的使用 |
6.2 PagingState 更新策略
在下拉刷新时,需要直接更新PagingController的value,替换原有数据,而非调用不存在的刷新方法,这是实现刷新功能的关键:
dart
// ❌ 错误:infinite_scroll_pagination无此类刷新方法,调用会报错
_pagingController.refreshPage(data, nextPageKey: 2);
_pagingController.refreshLastPage(data);
// ✅ 正确:直接更新PagingState,替换itemList和nextPageKey
_pagingController.value = PagingState(
itemList: data, // 刷新后的新数据
nextPageKey: isLastPage ? null : 2, // 新的下一页页码
);
6.3 状态重置保证
在下拉刷新和上拉加载的异步请求中,必须在finally代码块中重置状态变量,无论请求成功或失败,都能确保状态恢复,避免因异常导致的状态锁死:
dart
Future<void> _onRefresh() async {
_isRefreshing = true; // 标记刷新开始
try {
// 业务逻辑:请求数据、更新UI
await _fetchData();
_refreshController.refreshCompleted();
} catch (e) {
// 异常处理:标记刷新失败、显示提示
_refreshController.refreshFailed();
} finally {
_isRefreshing = false; // 无论成败,重置状态(一定执行)
}
}
常见问题排查
在鸿蒙Flutter开发中实现刷新加载功能,容易遇到以下问题,可针对性排查:
- 刷新/加载卡住 :检查状态变量是否在
finally块中重置,是否存在状态共用的情况; - 分页数据重复 :检查页码计算逻辑,确保
offset偏移量正确,避免每页请求相同数据; - 模拟器中应用崩溃 :检查是否释放了控制器资源(
dispose方法),是否处理了数据空值; - 图片加载失败:检查Picsum API的URL是否正确,是否处理了网络延迟和异常;
- 组件兼容问题 :确保
pull_to_refresh和infinite_scroll_pagination的版本与Flutter鸿蒙版兼容。
测试与部署
测试
本项目的所有功能均在DevEco Studio 6.0.0模拟器中进行测试,测试重点包括:
- 下拉刷新:连续多次刷新,检查是否卡顿、数据是否更新;
- 上拉加载:多次上拉,检查分页是否正常、数据是否重复;
- 状态提示:断网测试加载失败、加载中、无更多数据的提示是否正常;
- 兼容性:检查不同鸿蒙模拟器版本下应用是否正常运行。
部署
- 在DevEco Studio中配置鸿蒙应用的签名信息;
- 将模拟器切换为目标鸿蒙设备,执行运行命令;
- 测试通过后,可通过DevEco Studio打包为HAP文件,发布到鸿蒙应用市场。
项目总结
本项目基于OpenHarmony平台,使用Flutter鸿蒙版实现了具备流畅下拉刷新和上拉加载功能的风景图库应用,核心收获如下:
- 掌握了Flutter鸿蒙版开发中下拉刷新 和上拉加载 的核心技术选型,学会了
pull_to_refresh和infinite_scroll_pagination的组合使用; - 理解了分层架构在Flutter开发中的应用,实现了业务逻辑与UI的解耦,提升了代码的可维护性;
- 掌握了避免刷新加载冲突的状态分离设计,以及状态重置的关键技巧,解决了列表交互中的常见问题;
- 熟悉了OpenHarmony平台下Flutter应用的开发、调试与模拟器验证流程,为后续鸿蒙跨平台开发奠定基础。
在实际开发中,可基于本项目的基础,扩展更多功能,如图片点击预览、图片分类、下拉刷新样式自定义等,同时可结合Flutter的状态管理框架(如Provider、Bloc)实现更复杂的业务逻辑,打造更完善的鸿蒙应用。
OpenHarmony生态的发展为Flutter跨平台开发提供了新的场景,开发者可充分利用Flutter的生态优势,结合鸿蒙平台的特性,开发出高性能、高体验的跨端应用。
✨ 坚持用 清晰的图解 +易懂的硬件架构 + 硬件解析, 让每个知识点都 简单明了 !
🚀 个人主页 :一只大侠的侠 · CSDN
💬 座右铭 : "所谓成功就是以自己的方式度过一生。"
