相关阅读
关注微信公众号 糖果代码铺
,获取 Flutter
最新动态。
前言
有些感慨,现在群里也会不断有新人,会问一些简单的问题。大家都是从萌新过来的,当然问问题也要注意方式,如何问问题也是一门艺术。然后老油条们也应该更友善一些,多给新人一些建议。有新人肯定是好事,至少
flutter
还没有凉嘛。
最近有同学,在使用 pub-web.flutter-io.cn/packages/lo… 的时候会有一些疑问。
6
年前,从微软的 ISupportIncrementalLoading
中得到灵感,创造了属于 Flutter
平台的加载更多组件 pub-web.flutter-io.cn/packages/lo… 。这个组件一直都是蛮稳定的,所以介绍的文章就比较少了,最近(8个月前)新增了一些功能,随带就一起讲讲吧。
使用
为了照顾一下第一次使用这个组件的同学,我下面还是简单的介绍一下这个组件。
UI 和 数据 之间的契约
LoadingMoreBase
是组件提供的一个基类,用来给用户加载实际数据使用的。
实际开发中,你只需要继承它,并且实现 loadData
方法即可。当然你也需要关注 hasMore
这个属性,通过它来告诉组件是否还有更多的数据。
class TuChongRepository extends LoadingMoreBase<TuChongItem> {
TuChongRepository({this.maxLength = 300});
int _pageIndex = 1;
bool _hasMore = true;
@override
bool get hasMore => _hasMore && length < maxLength;
final int maxLength;
@override
Future<bool> refresh([bool notifyStateChanged = false]) async {
_hasMore = true;
_pageIndex = 1;
final bool result = await super.refresh(true);
return result;
}
@override
Future<bool> loadData([bool isLoadMoreAction = false]) async {
bool isSuccess = false;
try {
// 从服务端加载数据 feedList
for (final TuChongItem item in feedList!) {
if (item.hasImage && !contains(item) && hasMore) {
add(item);
}
}
_hasMore = feedList.isNotEmpty;
_pageIndex++;
isSuccess = true;
} catch (exception, stack) {
isSuccess = false;
print(exception);
print(stack);
}
return isSuccess;
}
}
怎么创建一个加载更多列表
最简单一个加载更多 UI
代码如下图。
LoadingMoreList(
ListConfig<TuChongItem>(
itemBuilder: ItemBuilder.itemBuilder,
sourceList: listSourceRepository,
),
),
当然我们也支持其他列表:
GridView
LoadingMoreList(
ListConfig<TuChongItem>(
itemBuilder: ItemBuilder.itemBuilder,
sourceList: listSourceRepository,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 3.0,
mainAxisSpacing: 3.0,
),
),
),
WaterfallFlow (瀑布流)
LoadingMoreList(
ListConfig<TuChongItem>(
extendedListDelegate: SliverWaterfallFlowDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 5,
mainAxisSpacing: 5,
),
itemBuilder: _buildItem,
sourceList: listSourceRepository,
),
),
Sliver
系列
通过使用 LoadingMoreCustomScrollView
,你可以加载任意的 Sliver
列表。
LoadingMoreCustomScrollView(
slivers: <Widget>[
SliverAppBar(
pinned: true,
title: Text("MultipleSliverDemo"),
),
// SliverList
LoadingMoreSliverList(SliverListConfig<TuChongItem>(
itemBuilder: ItemBuilder.itemBuilder,
sourceList: listSourceRepository,
)),
// SliverGrid
LoadingMoreSliverList(
SliverListConfig<TuChongItem>(
itemBuilder: ItemBuilder.itemBuilder,
sourceList: listSourceRepository1,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 3.0,
mainAxisSpacing: 3.0,
),
),
),
// SliverWaterfallFlow
LoadingMoreSliverList(
SliverListConfig<TuChongItem>(
itemBuilder: buildWaterfallFlowItem,
sourceList: listSourceRepository2,
extendedListDelegate: SliverWaterfallFlowDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 5,
mainAxisSpacing: 5,
),
),
),
],
),
自定义状态效果
加载更多有多种状态,你可以通过自定义 indicatorBuilder
来自定义自己的状态效果。
enum IndicatorStatus {
none,
// 加载更多
loadingMoreBusying,
// 列表为空时候的第一次全屏加载
fullScreenBusying,
// 加载更多报错
error,
// 列表为空时候的第一次全屏加载报错
fullScreenError,
// 没有更多数据加载
noMoreLoad,
// 空列表
empty
}
LoadingMoreList(
ListConfig<TuChongItem>(
itemBuilder: ItemBuilder.itemBuilder,
sourceList: listSourceRepository,
indicatorBuilder: _buildIndicator,
padding: EdgeInsets.all(0.0),
),
),
Widget _buildIndicator(BuildContext context, IndicatorStatus status) {
//if your list is sliver list ,you should build sliver indicator for it
//isSliver=true, when use it in sliver list
bool isSliver = false;
Widget widget;
switch (status) {
case IndicatorStatus.None:
widget = Container(height: 0.0);
break;
case IndicatorStatus.LoadingMoreBusying:
widget = Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
margin: EdgeInsets.only(right: 5.0),
height: 15.0,
width: 15.0,
child: getIndicator(context),
),
Text("正在加载...不要着急")
],
);
widget = _setbackground(false, widget, 35.0);
break;
case IndicatorStatus.FullScreenBusying:
widget = Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
margin: EdgeInsets.only(right: 0.0),
height: 30.0,
width: 30.0,
child: getIndicator(context),
),
Text("正在加载...不要着急")
],
);
widget = _setbackground(true, widget, double.infinity);
if (isSliver) {
widget = SliverFillRemaining(
child: widget,
);
} else {
widget = CustomScrollView(
slivers: <Widget>[
SliverFillRemaining(
child: widget,
)
],
);
}
break;
case IndicatorStatus.Error:
widget = Text(
"好像出现了问题呢?",
);
widget = _setbackground(false, widget, 35.0);
widget = GestureDetector(
onTap: () {
listSourceRepository.errorRefresh();
},
child: widget,
);
break;
case IndicatorStatus.FullScreenError:
widget = Text(
"好像出现了问题呢?",
);
widget = _setbackground(true, widget, double.infinity);
widget = GestureDetector(
onTap: () {
listSourceRepository.errorRefresh();
},
child: widget,
);
if (isSliver) {
widget = SliverFillRemaining(
child: widget,
);
} else {
widget = CustomScrollView(
slivers: <Widget>[
SliverFillRemaining(
child: widget,
)
],
);
}
break;
case IndicatorStatus.NoMoreLoad:
widget = Text("没有更多的了。。不要拖了");
widget = _setbackground(false, widget, 35.0);
break;
case IndicatorStatus.Empty:
widget = EmptyWidget(
"这里是空气!",
);
widget = _setbackground(true, widget, double.infinity);
if (isSliver) {
widget = SliverToBoxAdapter(
child: widget,
);
} else {
widget = CustomScrollView(
slivers: <Widget>[
SliverFillRemaining(
child: widget,
)
],
);
}
break;
}
return widget;
}
新功能
支持 center
center
其实蛮有用处的,具体的原理可以查看:
有的同学会问,为什么要支持这个,做什么呢,很简单,可以做个聊天列表,向上翻的时候就是加载更多的历史数据。
- 向上滚动的列表即历史数据,滚动到顶部会自动加载更多的历史数据。
- 正向的列表就是加载当前获取到的聊天数据。
return LoadingMoreCustomScrollView(
showGlowLeading: false,
center: _centerKey,
slivers: <Widget>[
// 历史数据
LoadingMoreSliverList<TuChongItem>(
SliverListConfig<TuChongItem>(
itemBuilder: itemBuilder,
sourceList: listSourceRepository1,
),
),
// 当前数据
LoadingMoreSliverList<TuChongItem>(
SliverListConfig<TuChongItem>(
itemBuilder: itemBuilder,
sourceList: listSourceRepository2,
),
key: _centerKey,
),
],
);
妈妈再也不会担心我不会写聊天列表了!
完整例子: github.com/fluttercand…
当然,也提供另外一种加载历史数据的方式,使用的是下拉刷新组件。这样你可以模拟出下拉回弹等动画。
PullToRefreshNotification(
onRefresh: onRefresh,
maxDragOffset: 48,
armedDragUpCancel: false,
child: CustomScrollView(
/// in case list is not full screen and remove ios Bouncing
physics: const AlwaysScrollableClampingScrollPhysics(),
controller: _scrollController,
center: _centerKey,
slivers: <Widget>[
// 加载历史数据的效果
PullToRefreshContainer(
(PullToRefreshScrollNotificationInfo? info) {
final double offset = info?.dragOffset ?? 0.0;
//loading history data
return SliverToBoxAdapter(
child: Container(
height: offset,
alignment: Alignment.center,
child: const CupertinoActivityIndicator(color: Colors.blue),
),
);
},
),
ExtendedSliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
final ChatItem item = newChats[index];
return buildItem(item);
},
childCount: newChats.length,
),
extendedListDelegate: const ExtendedListDelegate(),
),
ExtendedSliverList(
key: _centerKey,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
final ChatItem item = chats[index];
return buildItem(item);
},
childCount: chats.length,
),
extendedListDelegate: const ExtendedListDelegate(),
),
],
),
);
完整例子: github.com/fluttercand…
支持 LoadingMoreSliverList
封装处理
在实际使用中,经常有小伙伴会给 LoadingMoreSliverList
进行封装,比如
class MyLoadingMoreSliverList1 extends StatelessWidget {
const MyLoadingMoreSliverList1({
Key? key,
required this.listSourceRepository,
}) : super(key: key);
final TuChongRepository listSourceRepository;
@override
Widget build(BuildContext context) {
return SliverPadding(
padding: const EdgeInsets.all(50),
sliver: LoadingMoreSliverList<TuChongItem>(
SliverListConfig<TuChongItem>(
itemBuilder: itemBuilder,
sourceList: listSourceRepository,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 3.0,
mainAxisSpacing: 3.0,
),
),
),
);
}
}
由于之前列表的 config
是通过 LoadingMoreCustomScrollView
的 slivers
判断是否是 LoadingMoreSliverList
获取的。封装就会导致组件没法获取对应的配置。
现在你只需要将 LoadingMoreCustomScrollView.getConfigFromSliverContext
设置成 true
即可。
return LoadingMoreCustomScrollView(
// support LoadingMoreCustomScrollView.slivers are not a direct LoadingMoreSliverList
getConfigFromSliverContext: true,
slivers: <Widget>[
MyLoadingMoreSliverList1(
listSourceRepository: listSourceRepository1,
)
],
);
支持 处理单次加载的内容
如果
LoadingMoreCustomScrollView
里面有Sliver
只需要加载一次数据,我应该怎么写呢?
我想这应该也是实际开发会遇到的问题吧?新版本提供了 SliverLoadingData
和 SliveLoadingConfig
和 LoadingMoreLoadingSliver
来加载一次性数据,当然也支持定义加载动画以及失败效果,你也可以增加错误重试点击效果。
- 定义
数据
部分
class MySliverLoadingData extends SliverLoadingData<int> {
@override
Future<int?> onLoadData() async {
await Future<void>.delayed(const Duration(seconds: 5));
// retrun null means error
return 123456;
}
}
late MySliverLoadingData loadingData= MySliverLoadingData();
- 定义
UI
部分
return LoadingMoreCustomScrollView(
slivers: <Widget>[
LoadingMoreLoadingSliver<int>(
SliveLoadingConfig<int>(
builder: (BuildContext context, int? data) {
return SliverToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Loading Data$data'),
color: Colors.blue,
height: 50.0,
),
);
},
loadingData: loadingData,
),
),
],
);
结语
pub-web.flutter-io.cn/packages/lo… 结合 pub-web.flutter-io.cn/packages/pu… 你可以做出任何效果的下拉刷新+列表加载更多的效果。
组件没有花时间去做一些非常炫酷效果,是因为当每个人拿到三方组件的时候,都应该对其做一定的封装,来满足各自公司的设计效果。不是不想做,而是希望组件尽量简单,可扩展性高。
法同学: 以前在
github
上开源代码,用户跟我说加个功能吧,我都会说好好好,但是时间一久也没想起来做。其实这样挺不好的。 现在用户跟我说加个功能吧,除非用户的建议真的很好到我想马上加这个功能的程度,否则我就会在issue
直接说as design
,抱歉我不想加,然后直接关闭了。作为一个有讨好倾向的人,这是我锻炼真诚和勇气的方式。
最后想说,任何东西的设计都没法完全满足全部人的需求。对的,你的列表根本没有加载到我的心趴上。
爱 Flutter
,爱糖果
,欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果QQ群
最最后放上 Flutter Candies 全家桶,真香。