Flutter 进阶:最佳实践 —— 刷新列表组件化 NRefreshListView
一、需求来源
项目中有很多很多的列表,一直在研究如何组件化列表,但是按照最初的想法实现后发现有这样或那样的缺点。最近灵光一闪,在原先基础上做了减法和扩展,基本能满足大部分的开发场景,分享给大家。因为牵扯到业务就没有动图演示了,直接上代码。
二、使用示例
...
/// 方案列表
class SchemeListPage extends StatefulWidget {
const SchemeListPage({
super.key,
this.arguments,
});
final Map<String, dynamic>? arguments;
@override
State<SchemeListPage> createState() => _SchemeListPageState();
}
class _SchemeListPageState extends State<SchemeListPage> {
/// 获取上个页面传的参数
/// userId --- 用户id
late Map<String, dynamic> arguments = widget.arguments ?? Get.arguments;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("$widget"),
actions: ['done',].map((e) => TextButton(
child: Text(e,
style: TextStyle(color: Colors.white),
),
onPressed: () => debugPrint(e),)
).toList(),
),
body: buildBody(),
);
}
buildBody() {
return NRefreshListView<DepartmentPageDetailModel>(
pageSize: 2,
onRequest: (bool isRefresh, int page, int pageSize, last) async {
return await requestList(pageNo: page, pageSize: pageSize);
},
itemBuilder: (BuildContext context, int index, e) {
return InkWell(
onTap: () {
YLog.d("${e.toJson()}");
},
child: SchemeCell(
model: e,
index: index,
),
);
},
);
}
/// 列表数据请求
Future<List<DepartmentPageDetailModel>> requestList({
required int pageNo,
int pageSize = 20,
}) async {
var api = SchemePageApi(
ownerId: arguments['userId'] ?? '',
pageNo: pageNo,
pageSize: pageSize,
);
Map<String, dynamic>? response = await api.startRequest();
if (response['code'] != 'OK') {
return [];
}
final rootModel = DepartmentPageRootModel.fromJson(response ?? {});
var list = rootModel.result?.content ?? [];
return list;
}
}
三、源码
//
// NRefreshListView.dart
// flutter_templet_project
//
// Created by shang on 2024/3/8 10:59.
// Copyright © 2024/3/8 shang. All rights reserved.
//
import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart';
import 'package:flutter_templet_project/util/color_util_new.dart';
import 'package:flutter_templet_projectp/widget/n_placeholder.dart';
import 'package:flutter_templet_project/widget/n_skeleton_screen.dart';
typedef ValueIndexedWidgetBuilder<T> = Widget Function(
BuildContext context, int index, T data);
/// 请求列表回调
typedef RequestListCallback<T> = Future<List<T>> Function(
bool isRefresh, int pageNo, int pageSize, T? last,);
/// 刷新列表组件化
class NRefreshListView<T> extends StatefulWidget {
const NRefreshListView({
super.key,
this.controller,
this.child,
required this.onRequest,
this.onRequestError,
this.pageSize = 20,
this.pageNoInitial = 1,
this.disableOnReresh = false,
this.disableOnLoad = false,
this.needRemovePadding = false,
required this.itemBuilder,
this.separatorBuilder,
this.errorBuilder,
this.cachedChild,
this.refreshController,
});
/// 控制器
final NRefreshListViewController<T>? controller;
/// 子视图(为空 默认 带刷新组件的 ListView)
final Widget? child;
/// 刷新页面不变的部分,
final Widget? cachedChild;
/// 每页数量
final int pageSize;
/// 页面初始索引
final int pageNoInitial;
/// 禁用下拉刷新
final bool disableOnReresh;
/// 禁用上拉加载
final bool disableOnLoad;
/// 使用使用 MediaQuery.removePadding
final bool needRemovePadding;
/// 请求方法
final RequestListCallback<T> onRequest;
/// 请求错误方法
final void Function(Object error, StackTrace stack)? onRequestError;
/// 错误视图构建器
final TransitionBuilder? errorBuilder;
/// ListView 的 itemBuilder
final ValueIndexedWidgetBuilder<T> itemBuilder;
/// ListView 的 separatorBuilder
final IndexedWidgetBuilder? separatorBuilder;
/// 刷新控制器
final EasyRefreshController? refreshController;
@override
NRefreshListViewState<T> createState() => NRefreshListViewState<T>();
}
class NRefreshListViewState<T> extends State<NRefreshListView<T>> {
late final _easyRefreshController = widget.refreshController ??
EasyRefreshController(
controlFinishRefresh: true,
controlFinishLoad: true,
);
final _scrollController = ScrollController();
var indicator = IndicatorResult.none;
late var pageNo = widget.pageNoInitial;
late final items = ValueNotifier(<T>[]);
/// 首次加载
var isFirstLoad = true;
@override
void initState() {
super.initState();
widget.onRequest(true, pageNo, widget.pageSize, null).then((value) {
items.value = value;
isFirstLoad = false;
});
}
@override
Widget build(BuildContext context) {
if (isFirstLoad) {
return const NSkeletonScreen();
}
return buildBody();
}
buildBody() {
return ValueListenableBuilder<List<T>>(
valueListenable: items,
builder: (context, list, child) {
if (list.isEmpty) {
return NPlaceholder(
onTap: onLoad,
);
}
return buildRefresh(
child: widget.child ??
buildListView(
controller: _scrollController,
needRemovePadding: widget.needRemovePadding,
items: list,
),
);
},
);
}
Widget buildRefresh({
Widget? child,
}) {
return EasyRefresh(
controller: _easyRefreshController,
triggerAxis: Axis.vertical,
onRefresh: widget.disableOnReresh ? null : () => onRefresh(),
onLoad: widget.disableOnLoad || indicator == IndicatorResult.noMore
? null
: () => onLoad(),
child: child,
);
}
onRefresh() async {
pageNo = widget.pageNoInitial;
items.value = await widget.onRequest(true, pageNo, widget.pageSize, null);
indicator = items.value.length < widget.pageSize
? IndicatorResult.noMore
: IndicatorResult.success;
_easyRefreshController.finishRefresh(indicator);
}
onLoad() async {
if (!mounted) {
return;
}
if (indicator == IndicatorResult.noMore) {
return;
}
pageNo += 1;
final models = await widget.onRequest(
false,
pageNo,
widget.pageSize,
items.value.isNotEmpty ? items.value.last : null);
items.value = [...items.value, ...models];
indicator = models.length < widget.pageSize
? IndicatorResult.noMore
: IndicatorResult.success;
_easyRefreshController.finishLoad(indicator);
}
Widget buildListView({
ScrollController? controller,
bool needRemovePadding = false,
required List<T> items,
}) {
Widget child = Scrollbar(
controller: controller,
child: ListView.separated(
controller: controller,
itemCount: items.length,
itemBuilder: (context, index) =>
widget.itemBuilder(context, index, items[index]),
separatorBuilder: widget.separatorBuilder ??
(context, index) {
return const Divider(
color: Color(0xffe4e4e4),
height: 1,
);
},
),
);
if (needRemovePadding) {
child = MediaQuery.removePadding(
removeTop: true,
removeBottom: true,
context: context,
child: child,
);
}
return child;
}
}
/// NRefreshListView 组件控制器,将 NRefreshListViewState 的私有属性或者方法暴漏出去
class NRefreshListViewController<E> {
NRefreshListViewState<E>? _anchor;
List<E> get items {
assert(_anchor != null);
return _anchor!.items.value;
}
void onRefresh() {
assert(_anchor != null);
_anchor!.onRefresh();
}
void _attach(NRefreshListViewState<E> anchor) {
_anchor = anchor;
}
void _detach(NRefreshListViewState<E> anchor) {
if (_anchor == anchor) {
_anchor = null;
}
}
}
四、总结
1、NRefreshListView 是构思了很长时间才研究出来的组件
使用之后一个列表只需要一个列表请求方法,一个 NRefreshListView 组件调用,代码极其简单,只需要创建一下模型,配置一下 api 请求参数即可。
从此刻开始,十分钟一个列表不是梦!
2、NRefreshListView 封装了
- 下拉刷新,上拉加载;
- 首屏加载有 NSkeletonScreen;
- 数据为空有 NPlaceholder;
- 通过 child 可传入任何滚动视图;
- 通过 itemBuilder 将 ListView 的索引和模型透传到外部,子项随意定制;
3、如果想进一步提高开发效率需要自己开发批量 API 文件生成工具和 json 转 model 工具配合使用。至此,列表开发效率已经到了一个新的瓶颈:API参数还得手动配置。
初步思路:后端接口文档返回 json 固定格式,通过 json 转 API 工具自动生成API文件(应该包含 url,参数及参数类型,必填校验等等)。需要后端配合,后面再说。
转载自:https://juejin.cn/post/7353459702929424447