Flutter for OpenHarmony 实战:Pagination 分页
将分页功能封装为独立的Pagination组件,提高代码复用性和可维护性使用工厂构造函数模式解决非常量默认值的问题提供丰富的自定义参数,满足不同场景的需求。
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
前言:跨生态开发的新机遇
在移动开发领域,我们总是面临着选择与适配。今天,你的Flutter应用在Android和iOS上跑得正欢,明天可能就需要考虑一个新的平台:HarmonyOS(鸿蒙)。这不是一道选答题,而是很多团队正在面对的现实。
Flutter的优势很明确——写一套代码,就能在两个主要平台上运行,开发体验流畅。而鸿蒙代表的是下一个时代的互联生态,它不仅仅是手机系统,更着眼于未来全场景的体验。将现有的Flutter应用适配到鸿蒙,听起来像是一个“跨界”任务,但它本质上是一次有价值的技术拓展:让产品触达更多用户,也让技术栈覆盖更广。
不过,这条路走起来并不像听起来那么简单。Flutter和鸿蒙,从底层的架构到上层的工具链,都有着各自的设计逻辑。会遇到一些具体的问题:代码如何组织?原有的功能在鸿蒙上如何实现?那些平台特有的能力该怎么调用?更实际的是,从编译打包到上架部署,整个流程都需要重新摸索。
这篇文章想做的,就是把这些我们趟过的路、踩过的坑,清晰地摊开给你看。我们不会只停留在“怎么做”,还会聊到“为什么得这么做”,以及“如果出了问题该往哪想”。这更像是一份实战笔记,源自真实的项目经验,聚焦于那些真正卡住过我们的环节。
无论你是在为一个成熟产品寻找新的落地平台,还是从一开始就希望构建能面向多端的应用,这里的思路和解决方案都能提供直接的参考。理解了两套体系之间的异同,掌握了关键的衔接技术,不仅能完成这次迁移,更能积累起应对未来技术变化的能力。
混合工程结构深度解析
项目目录架构
当Flutter项目集成鸿蒙支持后,典型的项目结构会发生显著变化。以下是经过ohos_flutter插件初始化后的项目结构:
my_flutter_harmony_app/
├── lib/ # Flutter业务代码(基本不变)
│ ├── main.dart # 应用入口
│ ├── home_page.dart # 首页
│ └── utils/
│ └── platform_utils.dart # 平台工具类
├── pubspec.yaml # Flutter依赖配置
├── ohos/ # 鸿蒙原生层(核心适配区)
│ ├── entry/ # 主模块
│ │ └── src/main/
│ │ ├── ets/ # ArkTS代码
│ │ │ ├── MainAbility/
│ │ │ │ ├── MainAbility.ts # 主Ability
│ │ │ │ └── MainAbilityContext.ts
│ │ │ └── pages/
│ │ │ ├── Index.ets # 主页面
│ │ │ └── Splash.ets # 启动页
│ │ ├── resources/ # 鸿蒙资源文件
│ │ │ ├── base/
│ │ │ │ ├── element/ # 字符串等
│ │ │ │ ├── media/ # 图片资源
│ │ │ │ └── profile/ # 配置文件
│ │ │ └── en_US/ # 英文资源
│ │ └── config.json # 应用核心配置
│ ├── ohos_test/ # 测试模块
│ ├── build-profile.json5 # 构建配置
│ └── oh-package.json5 # 鸿蒙依赖管理
└── README.md
展示效果图片
flutter 实时预览 效果展示
运行到鸿蒙虚拟设备中效果展示
目录
功能代码实现
分页组件
分页组件是本次开发的核心功能,实现了灵活的分页控制和交互效果,可直接在页面中使用,无需通过按钮导航。
核心设计
- 支持完整的页码控制:首页、上一页、下一页、末页按钮
- 智能页码列表显示,带有省略号处理,优化用户体验
- 支持每页显示条数调整,提供多种预设选项
- 支持快速跳转到指定页码,提高操作效率
- 丰富的点击交互效果,提升用户体验
- 高度定制化配置,支持自定义颜色、按钮样式等
- 响应式设计,适配不同屏幕尺寸
实现细节
构造函数设计
分页组件采用了工厂构造函数模式,解决了非常量默认值的问题,确保组件初始化的灵活性:
class Pagination extends StatelessWidget {
final int currentPage;
final int totalPages;
final int pageSize;
final int totalItems;
final Function(int) onPageChanged;
final bool showTotal;
final bool showSizeChanger;
final List<int> sizeOptions;
final Function(int)? onSizeChanged;
final bool showQuickJumper;
final Color activeColor;
final Color inactiveColor;
final Color disabledColor;
final double buttonRadius;
const Pagination._({
required this.currentPage,
required this.totalPages,
required this.pageSize,
required this.totalItems,
required this.onPageChanged,
required this.showTotal,
required this.showSizeChanger,
required this.sizeOptions,
required this.onSizeChanged,
required this.showQuickJumper,
required this.activeColor,
required this.inactiveColor,
required this.disabledColor,
required this.buttonRadius,
Key? key,
}) : super(key: key);
factory Pagination({
Key? key,
required int currentPage,
required int totalPages,
int pageSize = 10,
int totalItems = 0,
required Function(int) onPageChanged,
bool showTotal = true,
bool showSizeChanger = false,
List<int> sizeOptions = const [10, 20, 50, 100],
Function(int)? onSizeChanged,
bool showQuickJumper = false,
Color activeColor = Colors.deepPurple,
Color inactiveColor = Colors.grey,
Color? disabledColor,
double buttonRadius = 4.0,
}) {
return Pagination._(
key: key,
currentPage: currentPage,
totalPages: totalPages,
pageSize: pageSize,
totalItems: totalItems,
onPageChanged: onPageChanged,
showTotal: showTotal,
showSizeChanger: showSizeChanger,
sizeOptions: sizeOptions,
onSizeChanged: onSizeChanged,
showQuickJumper: showQuickJumper,
activeColor: activeColor,
inactiveColor: inactiveColor,
disabledColor: disabledColor ?? Colors.grey[300]!,
buttonRadius: buttonRadius,
);
}
// 其他方法...
}
技术要点:
- 使用私有构造函数
Pagination._确保组件参数的完整性 - 通过工厂构造函数
Pagination提供默认值和参数处理 - 解决了
Colors.grey[300]非常量的问题,通过工厂构造函数延迟初始化
布局实现
分页组件采用了响应式布局设计,使用 SingleChildScrollView 解决水平溢出问题:
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[200]!),
borderRadius: BorderRadius.circular(8.0),
color: Colors.white,
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 左侧:总数和每页大小
Row(
children: [
if (showTotal)
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Text(
'共 $totalItems 条记录',
style: const TextStyle(fontSize: 14.0),
),
),
if (showSizeChanger && onSizeChanged != null)
Row(
children: [
const Text('每页显示:', style: TextStyle(fontSize: 14.0)),
DropdownButton<int>(
value: pageSize,
onChanged: (value) {
if (value != null) {
_handleSizeChange(value);
}
},
items: sizeOptions.map((size) {
return DropdownMenuItem<int>(
value: size,
child: Text('$size 条'),
);
}).toList(),
style: const TextStyle(fontSize: 14.0),
underline: Container(),
),
],
),
],
),
const SizedBox(width: 16.0),
// 右侧:页码控制
Row(
children: [
// 首页按钮
_buildPageButton(
'首页',
onPressed: currentPage > 1 ? () => _handlePageChange(1) : null,
),
const SizedBox(width: 8.0),
// 上一页按钮
_buildPageButton(
'上一页',
onPressed: currentPage > 1 ? () => _handlePageChange(currentPage - 1) : null,
),
const SizedBox(width: 16.0),
// 页码列表
_buildPageNumbers(),
const SizedBox(width: 16.0),
// 下一页按钮
_buildPageButton(
'下一页',
onPressed: currentPage < totalPages ? () => _handlePageChange(currentPage + 1) : null,
),
const SizedBox(width: 8.0),
// 末页按钮
_buildPageButton(
'末页',
onPressed: currentPage < totalPages ? () => _handlePageChange(totalPages) : null,
),
// 快速跳转
if (showQuickJumper)
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Row(
children: [
const Text('前往', style: TextStyle(fontSize: 14.0)),
Container(
width: 60.0,
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: TextField(
keyboardType: TextInputType.number,
onSubmitted: (value) {
final page = int.tryParse(value);
if (page != null) {
_handlePageChange(page);
}
},
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
),
),
),
Text('页', style: TextStyle(fontSize: 14.0)),
],
),
),
],
),
],
),
),
);
}
技术要点:
- 使用
SingleChildScrollView包裹分页控件,解决小屏幕设备上的水平溢出问题 - 采用
Row和MainAxisAlignment.spaceBetween实现左右布局,左侧显示统计信息,右侧显示页码控制 - 使用
Padding和SizedBox控制组件间距,提高布局美观度
按钮和页码实现
分页组件的核心在于按钮和页码的实现,包括点击效果和状态管理:
Widget _buildPageButton(String text, {VoidCallback? onPressed}) {
final isDisabled = onPressed == null;
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(buttonRadius),
border: Border.all(
color: isDisabled ? disabledColor : inactiveColor,
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(buttonRadius),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0),
child: Text(
text,
style: TextStyle(
color: isDisabled ? disabledColor : inactiveColor,
fontSize: 14.0,
),
),
),
),
),
);
}
Widget _buildPageNumbers() {
final List<Widget> pageWidgets = [];
final int startPage = currentPage > 3 ? currentPage - 2 : 1;
final int endPage = startPage + 4 > totalPages ? totalPages : startPage + 4;
// 显示省略号
if (startPage > 1) {
pageWidgets.add(
_buildPageButton('1', onPressed: () => _handlePageChange(1)),
);
if (startPage > 2) {
pageWidgets.add(
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Text('...', style: TextStyle(fontSize: 14.0)),
),
);
}
}
// 显示页码
for (int i = startPage; i <= endPage; i++) {
pageWidgets.add(
Container(
margin: const EdgeInsets.symmetric(horizontal: 2.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(buttonRadius),
color: i == currentPage ? activeColor : Colors.transparent,
border: Border.all(
color: i == currentPage ? activeColor : inactiveColor,
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _handlePageChange(i),
borderRadius: BorderRadius.circular(buttonRadius),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 6.0),
child: Text(
'$i',
style: TextStyle(
color: i == currentPage ? Colors.white : inactiveColor,
fontSize: 14.0,
fontWeight: i == currentPage ? FontWeight.bold : FontWeight.normal,
),
),
),
),
),
),
);
}
// 显示省略号
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
pageWidgets.add(
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Text('...', style: TextStyle(fontSize: 14.0)),
),
);
}
pageWidgets.add(
_buildPageButton('$totalPages', onPressed: () => _handlePageChange(totalPages)),
);
}
return Row(children: pageWidgets);
}
技术要点:
- 使用
InkWell实现按钮的点击效果,提供视觉反馈 - 通过
isDisabled状态控制按钮的禁用状态和样式 - 实现智能页码计算逻辑,根据当前页码动态调整显示范围
- 使用省略号处理,优化大量页码时的显示效果
- 当前页码高亮显示,提高用户识别度
事件处理
分页组件通过回调函数实现与父组件的通信,处理页码变化和每页大小调整:
void _handlePageChange(int page) {
if (page >= 1 && page <= totalPages && page != currentPage) {
onPageChanged(page);
}
}
void _handleSizeChange(int size) {
if (onSizeChanged != null && size != pageSize) {
onSizeChanged!(size);
}
}
技术要点:
- 添加边界检查,确保页码在有效范围内
- 避免重复触发相同页码的回调,提高性能
- 使用可选回调
onSizeChanged处理每页大小变化
使用方法
在页面中直接使用 Pagination 组件,传入必要的参数和回调函数:
Pagination(
currentPage: _currentPage,
totalPages: _totalPages,
pageSize: _pageSize,
totalItems: _totalItems,
onPageChanged: _handlePageChanged,
showTotal: true,
showSizeChanger: true,
onSizeChanged: _handleSizeChanged,
showQuickJumper: true,
activeColor: Colors.deepPurple,
inactiveColor: Colors.grey,
disabledColor: Colors.grey[300]!,
)
注意事项
- 参数验证:确保传入的
currentPage和totalPages为有效值,避免出现负数或零 - 回调函数:必须实现
onPageChanged回调函数,否则页码变化不会触发任何操作 - 样式定制:根据应用主题调整
activeColor、inactiveColor等颜色参数,确保视觉一致性 - 屏幕适配:在小屏幕设备上,分页组件会自动水平滚动,确保完整显示
- 性能优化:当数据量较大时,建议在
onPageChanged回调中实现异步数据加载,避免阻塞 UI
首页集成使用
在首页中直接集成分页组件,展示完整的分页功能,无需通过按钮导航。
核心设计
- 添加美观的
AppBar:设置标题和背景色,提升应用整体美观度 - 实现数据列表:显示当前页的数据内容,支持奇偶行不同背景色
- 集成分页组件:展示完整的分页功能,包括页码控制、每页大小调整和快速跳转
- 添加数据加载逻辑:模拟分页数据加载,实现数据与页码的同步
实现细节
状态管理
首页使用 StatefulWidget 和 setState() 管理分页相关状态:
class _MyHomePageState extends State<MyHomePage> {
int _currentPage = 1;
int _totalPages = 20;
int _pageSize = 10;
int _totalItems = 200;
List<String> _dataList = [];
void initState() {
super.initState();
_loadData();
}
void _loadData() {
setState(() {
_dataList = List.generate(
_pageSize,
(index) => '第 $_currentPage 页 - 第 ${(index + 1)} 条数据',
);
});
}
void _handlePageChanged(int page) {
setState(() {
_currentPage = page;
_loadData();
});
}
void _handleSizeChanged(int size) {
setState(() {
_pageSize = size;
_currentPage = 1;
_totalPages = (_totalItems / size).ceil();
_loadData();
});
}
// 构建方法...
}
技术要点:
- 在
initState中初始化数据,确保页面加载时显示第一页数据 - 使用
setState()更新页面状态,触发 UI 重建 - 实现
_loadData方法模拟数据加载,根据当前页码和每页大小生成对应数据 - 在
_handleSizeChanged中重新计算总页数,确保分页逻辑正确
页面布局
首页采用 Column 布局,包含标题、数据列表和分页组件:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Pagination 分页示例'),
backgroundColor: Colors.deepPurple,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 20),
const Text(
'Pagination 分页示例',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.deepPurple,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
// 数据列表
Expanded(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[200]!),
borderRadius: BorderRadius.circular(8.0),
),
child: ListView.builder(
itemCount: _dataList.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(_dataList[index]),
tileColor: index % 2 == 0 ? Colors.grey[50] : Colors.white,
);
},
),
),
),
const SizedBox(height: 20),
// 分页组件
Pagination(
currentPage: _currentPage,
totalPages: _totalPages,
pageSize: _pageSize,
totalItems: _totalItems,
onPageChanged: _handlePageChanged,
showTotal: true,
showSizeChanger: true,
onSizeChanged: _handleSizeChanged,
showQuickJumper: true,
activeColor: Colors.deepPurple,
inactiveColor: Colors.grey,
disabledColor: Colors.grey[300]!,
),
],
),
),
);
}
技术要点:
- 使用
Scaffold作为页面骨架,提供AppBar和body - 使用
Column和Expanded实现自适应布局,确保数据列表占据可用空间 - 使用
ListView.builder构建数据列表,提高性能 - 实现奇偶行不同背景色,提高数据可读性
- 直接在
Column中嵌入Pagination组件,无需通过按钮导航
使用方法
首页集成非常简单,只需在 main.dart 文件中实现上述代码,应用启动后会直接显示带有分页功能的首页。
注意事项
- 数据初始化:确保在
initState中调用_loadData()方法,避免页面显示空白 - 状态同步:在
_handlePageChanged和_handleSizeChanged中,确保先更新状态再加载数据 - 总页数计算:使用
(_totalItems / size).ceil()计算总页数,确保向上取整 - UI 一致性:保持
AppBar背景色与分页组件的activeColor一致,提高视觉统一性 - 性能考虑:当数据量较大时,建议实现真正的分页加载逻辑,而不是模拟数据
开发中容易遇到的问题
1. 非常量默认值问题
问题描述:在定义组件构造函数时,使用 Colors.grey[300] 作为默认值会导致编译错误,因为 [] 操作符不能在常量表达式中使用。
解决方案:采用工厂构造函数模式,将非常量默认值的处理移至工厂方法中:
// 错误写法
const Pagination({
// ...
this.disabledColor = Colors.grey[300]!, // 编译错误
// ...
});
// 正确写法
factory Pagination({
// ...
Color? disabledColor,
// ...
}) {
return Pagination._(
// ...
disabledColor: disabledColor ?? Colors.grey[300]!,
// ...
);
}
2. 水平溢出问题
问题描述:在小屏幕设备上,分页组件的内容可能会超出屏幕宽度,导致布局溢出。
解决方案:使用 SingleChildScrollView 包裹分页组件,并设置 scrollDirection: Axis.horizontal:
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
// 分页组件内容
),
)
3. 页码计算错误
问题描述:在计算总页数或当前页码范围时,可能会出现计算错误,导致页码显示不正确。
解决方案:
- 使用正确的数学公式计算总页数:
(_totalItems / _pageSize).ceil() - 添加边界检查,确保页码在有效范围内
- 测试不同数据量和每页大小的组合,确保计算逻辑正确
4. 状态管理混乱
问题描述:在处理分页状态时,可能会出现状态管理混乱的问题,导致页码与数据不同步。
解决方案:
- 使用明确的状态管理方式,如
setState()或状态管理库 - 确保状态更新的一致性,先更新状态再加载数据
- 在回调函数中添加状态检查,避免无效的状态更新
5. 快速跳转输入验证
问题描述:在处理快速跳转输入时,可能会出现输入验证不足的问题,导致无效页码的提交。
解决方案:
- 在
onSubmitted回调中添加输入验证,确保输入的页码在有效范围内 - 使用
int.tryParse()处理非数字输入的情况 - 考虑添加输入框的
inputFormatters,限制只能输入数字
6. 跨平台兼容性问题
问题描述:在不同平台上,分页组件的显示效果可能会有所不同,特别是在 OpenHarmony 平台上。
解决方案:
- 使用 Flutter 的跨平台组件和 API,避免使用平台特定的功能
- 测试组件在不同平台上的显示效果,确保一致性
- 遵循 Flutter 的最佳实践,确保代码的跨平台兼容性
总结开发中用到的技术点
1. 组件封装技术
- 将分页功能封装为独立的
Pagination组件,提高代码复用性和可维护性 - 使用工厂构造函数模式解决非常量默认值的问题
- 提供丰富的自定义参数,满足不同场景的需求
2. 状态管理技术
- 使用
StatefulWidget和setState()管理页面状态 - 通过回调函数实现组件间的状态通信
- 确保状态更新的一致性和及时性
3. 布局设计技术
- 使用
Row、Column、Expanded等布局组件实现清晰的页面结构 - 使用
SingleChildScrollView解决水平溢出问题 - 使用
Container、BoxDecoration实现美观的界面效果 - 使用
Padding和SizedBox控制组件间距
4. 交互设计技术
- 使用
InkWell实现按钮的点击效果,提升用户体验 - 实现智能的页码显示逻辑,包括省略号处理
- 支持快速跳转功能,提高操作效率
- 提供明确的视觉反馈,如按钮禁用状态和页码高亮
5. 输入处理技术
- 使用
TextField实现快速跳转功能 - 添加输入验证,确保输入的有效性
- 使用
DropdownButton实现每页大小调整
6. 数据处理技术
- 实现数据加载逻辑,模拟分页数据的获取和显示
- 计算总页数和当前页码范围,确保分页逻辑的正确性
- 支持动态调整每页大小,自动重新计算总页数
7. 主题配置技术
- 使用
ThemeData和ColorScheme.fromSeed创建统一的应用主题 - 设置
AppBar的背景色,与应用主题保持一致 - 支持自定义分页组件的颜色,适应不同的应用主题
8. 跨平台兼容性技术
- 使用 Flutter 的跨平台组件和 API,确保在 OpenHarmony 等平台上正常运行
- 避免使用平台特定的功能,提高代码的可移植性
- 测试组件在不同平台上的显示效果,确保一致性
9. 性能优化技术
- 使用
ListView.builder构建数据列表,提高渲染性能 - 实现数据的按需加载,只加载当前页的数据
- 优化渲染性能,避免不必要的重建和重绘
- 在回调函数中添加状态检查,避免无效的状态更新
10. 用户体验优化技术
- 提供完整的页码控制功能,满足不同用户的操作习惯
- 实现智能页码显示,优化大量页码时的用户体验
- 支持快速跳转和每页大小调整,提高操作效率
- 提供明确的视觉反馈,如按钮点击效果和页码高亮
- 实现响应式设计,适配不同屏幕尺寸
通过本次开发,我们成功实现了一个功能完整、交互友好的分页组件,并直接集成到首页中展示。该组件不仅满足了基本的分页需求,还提供了丰富的自定义选项和优化的用户体验,完全适配 Flutter for OpenHarmony 平台的开发需求。
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)