Flutter for OpenHarmony 开发指南(四):实现上拉加载,下拉刷新能力
本文介绍了如何在Flutter应用中实现上拉加载和下拉刷新功能。通过使用RefreshIndicator组件处理下拉刷新,监听ListView的ScrollController实现上拉加载。文章详细讲解了状态管理、数据加载逻辑和UI构建方法,包括初始化设置、数据请求处理以及底部加载提示的实现。这种基于Flutter原生组件的方法简单高效,适合构建基本的列表页面功能。
前言
实现一个Flutter 应用中常见且核心的功能:上拉加载更多和下拉刷新。将从最基础的实现方式入手,使用 Flutter 内置的组件和控制器来构建这个功能。
核心思路
-
下拉刷新:使用 Flutter 官方提供的
RefreshIndicator组件。它能够监听子组件的下拉手势,并在触发时执行一个回调函数,在这个回调中加载最新的数据。 -
上拉加载:通过监听
ListView的ScrollController来实现。当用户滚动到列表底部附近时,判断是否需要加载下一页数据,并执行相应的数据请求和状态更新。
完整代码
import 'package:flutter/material.dart';
// 对联数据模型
class CoupletModel {
final String upper; // 上联
final String lower; // 下联
final String horizontal; // 横批
CoupletModel({
required this.upper,
required this.lower,
required this.horizontal,
});
}
class CoupletPage extends StatefulWidget {
const CoupletPage({super.key});
@override
State<CoupletPage> createState() => _CoupletPageState();
}
class _CoupletPageState extends State<CoupletPage> {
final List<CoupletModel> _dataList = []; // 数据源
final ScrollController _scrollController = ScrollController(); // 滚动控制器
int _page = 1; // 当前页码
bool _isLoading = false; // 是否正在加载
bool _hasMore = true; // 是否还有更多数据
@override
void initState() {
super.initState();
_refreshData(); // 初始化加载
// 监听滚动实现上拉加载
_scrollController.addListener(() {
// 当滚动到距离底部小于 100 像素时,触发加载更多
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 100) {
_loadMoreData();
}
});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
// 模拟网络请求方法
Future<List<CoupletModel>> _fetchMockData(int page) async {
await Future.delayed(const Duration(seconds: 30)); // 模拟网络延迟
// 模拟第 4 页之后没有更多数据了
if (page > 3) return [];
return List.generate(10, (index) {
int id = (page - 1) * 10 + index + 1;
return CoupletModel(
upper: "上联:岁岁平安节节高",
lower: "下联:年年如意步步升",
horizontal: "横批:大吉大利",
);
});
}
// 下拉刷新逻辑
Future<void> _refreshData() async {
setState(() {
_page = 1;
_hasMore = true;
});
List<CoupletModel> newData = await _fetchMockData(_page);
setState(() {
_dataList.clear();
_dataList.addAll(newData);
});
}
// 加载更多逻辑
Future<void> _loadMoreData() async {
if (_isLoading || !_hasMore) return;
setState(() {
_isLoading = true;
});
int nextPage = _page + 1;
List<CoupletModel> newData = await _fetchMockData(nextPage);
setState(() {
_isLoading = false;
if (newData.isEmpty) {
_hasMore = false;
} else {
_page = nextPage;
_dataList.addAll(newData);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("春节对联"),
backgroundColor: Colors.white,
foregroundColor: Colors.black,
),
body: RefreshIndicator(
onRefresh: _refreshData,
child: ListView.builder(
controller: _scrollController,
itemCount: _dataList.length + 1, // +1 是为了显示底部的加载状态
itemBuilder: (context, index) {
// 如果是最后一项,根据状态显示“加载中”或“没有更多”
if (index == _dataList.length) {
return _buildFooter();
}
final item = _dataList[index];
return _buildCoupletItem(item);
},
),
),
);
}
// 单个对联卡片布局
Widget _buildCoupletItem(CoupletModel item) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 3,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Text(
item.horizontal,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: Colors.red,
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [_verticalText(item.upper), _verticalText(item.lower)],
),
],
),
),
);
}
// 竖排文字组件
Widget _verticalText(String text) {
return SizedBox(
width: 30,
child: Text(
text.replaceAll(":", "\n"), // 把冒号转行
style: const TextStyle(
fontSize: 16,
height: 1.5,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
);
}
// 底部加载提示
Widget _buildFooter() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Center(
child: _hasMore
? const CircularProgressIndicator(strokeWidth: 2)
: const Text(
"--- 我是有底线的 ---",
style: TextStyle(color: Colors.grey),
),
),
);
}
}
代码实现
1. 整体页面布局 (Scaffold 和 AppBar)
这是页面的最外层框架,由build方法中的Scaffold widget构建。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("春节对联"),
backgroundColor: Colors.white,
foregroundColor: Colors.black,
),
body: RefreshIndicator(
// ... a child widget
),
);
}
-
Scaffold: 提供了标准的移动应用布局结构,包括顶部的应用栏(AppBar)和页面的主体内容(body)。 -
AppBar: 这是顶部的导航栏。-
title: const Text("春节对联"): 设置了导航栏的标题文字。 -
backgroundColor: Colors.white: 将背景色设为白色。 -
foregroundColor: Colors.black: 将AppBar中所有前景元素(包括标题文字和默认的返回按钮图标)的颜色设为黑色,以确保在白色背景下可见。
-
2.下拉刷新容器 (RefreshIndicator)
页面的主体body被一个RefreshIndicator包裹,这是实现下拉刷新功能的关键。
body: RefreshIndicator(
onRefresh: _refreshData,
child: ListView.builder(
// ...
),
),
-
RefreshIndicator: 这是一个内置的Widget,当它的子组件(child)被向下滑动到足够距离时,它会显示一个Material Design风格的刷新动画,并触发onRefresh回调。 -
onRefresh: _refreshData: 将UI手势与业务逻辑连接起来。当用户执行下拉刷新操作时,RefreshIndicator会自动调用之前定义好的_refreshData方法。 -
child:RefreshIndicator的子组件必须是可滚动的,这里放置了ListView.builder。
3. 动态列表 (ListView.builder)
这是页面的核心内容区域,负责高效地显示对联列表。
child: ListView.builder(
controller: _scrollController,
itemCount: _dataList.length + 1, // +1 是为了显示底部的加载状态
itemBuilder: (context, index) {
// 如果是最后一项,根据状态显示“加载中”或“没有更多”
if (index == _dataList.length) {
return _buildFooter();
}
final item = _dataList[index];
return _buildCoupletItem(item);
},
),
-
ListView.builder: 这是一个高性能的列表构建器,它只会在列表项即将进入屏幕时才创建(build)它们,非常适合长列表。 -
controller: _scrollController: 将创建的滚动控制器_scrollController与ListView关联。这样就能通过监听控制器来获知列表的滚动位置,从而实现上拉加载。 -
itemCount: _dataList.length + 1: 这是实现底部加载提示的一个巧妙技巧。列表的总长度被设置为“数据列表的长度 + 1”。这个多出来的“+1”项就是用来显示“加载中”或“没有更多了”的占位符。 -
itemBuilder: 这是一个函数,用于根据索引index构建每一项的UI。-
if (index == _dataList.length): 这是一个关键判断。当itemBuilder构建到最后一项(即那个“+1”的项)时,index正好等于数据列表的长度。这时,不渲染普通的对联卡片,而是调用_buildFooter()方法来渲染底部的加载提示。 -
else: 对于其他所有项,正常地从_dataList中取出数据,并调用_buildCoupletItem(item)来渲染一个对联卡片。
-
4. 底部加载提示 (_buildFooter)
用于构建列表最底部的UI,根据加载状态显示不同的内容。
Widget _buildFooter() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Center(
child: _hasMore
? const CircularProgressIndicator(strokeWidth: 2)
: const Text(
"--- 我是有底线的 ---",
style: TextStyle(color: Colors.grey),
),
),
);
}
-
Padding&Center: 用于提供一些垂直间距并让内容居中显示。 -
_hasMore ? ... : ...(三元运算符): 这是动态UI的核心。-
如果
_hasMore为true(意味着还有更多数据可以加载),则显示一个CircularProgressIndicator(圆形的加载动画)。 -
如果
_hasMore为false(意味着所有数据都已加载完毕),则显示一段文本"--- 我是有底线的 ---"。
-
5. 单个对联卡片 (_buildCoupletItem)
这个方法负责构建列表中每一张独立的对联卡片。
Widget _buildCoupletItem(CoupletModel item) {
return Card(
// ...
child: Padding(
// ...
child: Column(
children: [
Text(item.horizontal, /* ... styles ... */),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [_verticalText(item.upper), _verticalText(item.lower)],
),
],
),
),
);
}
-
Card: 作为卡片的根容器,它提供了Material Design风格的圆角、阴影和背景。 -
Padding: 在Card内部添加一些边距,让内容不会紧贴卡片边缘。 -
Column: 负责将卡片内容垂直排列:横批在上方,对联在下方。 -
Text(item.horizontal, ...): 显示横批,并设置了加粗、红色、大号字体的样式。 -
SizedBox(height: 10): 在横批和对联之间创建一个固定高度的间隙。 -
Row: 负责将上下联水平排列。mainAxisAlignment: MainAxisAlignment.spaceAround让上下联在水平方向上均匀分布空间。 -
_verticalText(...):Row的子组件是两个调用_verticalText方法生成的Widget,分别用于显示上联和下联。
6. 竖排文字组件 (_verticalText)
这是一个非常巧妙的自定义Widget,用于实现文字的竖向排列效果。
Widget _verticalText(String text) {
return SizedBox(
width: 30,
child: Text(
text.replaceAll(":", "\\n"), // 把冒号转行
style: const TextStyle(
fontSize: 16,
height: 1.5,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
);
}
-
SizedBox(width: 30, ...): 关键点之一。它强制限制了Text组件的宽度。 -
text.replaceAll(":", "\\n"): 实现竖排的核心技巧。它将文本中的冒号替换为换行符\n。由于SizedBox的宽度非常窄(只能容下一个字),Text组件在渲染时会自动换行。通过将字符间的“分隔符”变成换行符,就实现了每个字占一行的效果,从而形成了视觉上的竖排。 -
style:-
height: 1.5: 设置行高,增加了每个字之间的垂直间距,使其更美观。 -
textAlign: TextAlign.center: 确保每个字都在其狭窄的空间内居中。
-
效果预览

总结
通过 RefreshIndicator 和 ScrollController,用 Flutter 的原生方式实现了下拉刷新和上拉加载功能。
欢迎加入开源鸿蒙跨平台社区
https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)