Richard's Blog

无限动态列表

字数统计: 1k阅读时长: 4 min
2019/03/07 Share

本文来自于我自己学习Flutter时所学习的教程的中文翻译,原文链接INFINITE DYNAMIC LISTVIEW

英语水平有限,内容未必准确

在这篇文章中,我将快速介绍如何做一个无限的列表(ListView),当用户滑动到最底端时可以动态的加载更多数据。最终结果就像下面这样:

infinite_dynamic_list_view_1

让我们开始吧!

起点

让我们从一个简单的包含10个数字的列表开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
List<int> items = List.generate(10, (i) => i);

@override
void initState() {
super.initState();
}

@override
void dispose() {
super.dispose();
}

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: Text("Infinite ListView"),
),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(title: new Text("Number $index"));
},
),
);
}
}

动态数据加载

首先,我们需要创建一个方法模仿http请求。假设我们通过传递fromto两个参数,就可以得到一个在这两个数之间的结果。我们要加些延迟让这个方法更”网络”。方法看起来像下面这样:

1
2
3
4
5
6
/// from - inclusive, to - exclusive
Future<List<int>> fakeRequest(int from, int to) async {
return Future.delayed(Duration(seconds: 2), () {
return List.generate(to - from, (i) => i + from);
});
}

我们希望当用户滑动ListView到最底部的时候能调用这么方法。最简单的方法就是使用ScrollControllerScrollController会监听滑动行为,当用户滑动到最底部时我们让它发起一个请求。当正在发送请求时,注意预防我们的应用频繁发送(不等上一个请求返回就再发起一个请求)。我的方案是增加一个标识isPerformingRequest,当只有这个标识为false的时候才能发起一个请求。这部分的代码就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class _MyHomePageState extends State<MyHomePage> {
List<int> items = List.generate(10, (i) => i);
ScrollController _scrollController = new ScrollController();
bool isPerformingRequest = false;

@override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
_getMoreData();
}
});
}

@override
void dispose() {
_scrollController.dispose();
super.dispose();
}

_getMoreData() async {
if (!isPerformingRequest) {
setState(() => isPerformingRequest = true);
List<int> newEntries = await fakeRequest(items.length, items.length + 10);
setState(() {
items.addAll(newEntries);
isPerformingRequest = false;
});
}
}

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: Text("Infinite ListView"),
),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(title: new Text("Number $index"));
},
controller: _scrollController,
),
);
}
}

当我们运行app时,我们可以看到数据被动态加载了。然而,这个远没达到满意的效果。我们需要添加某种指示器来告知用户请求完成了。

infinite_dynamic_list_view_2

进度指示器

我们主要的组件应该是CircularProgressIndicator,它应该被包在CenterOpacityPadding组件中。当请求被发起时,我们准备使用Opacity组件显示我们的指示器。整个组件看起来是这样:

1
2
3
4
5
6
7
8
9
10
11
Widget _buildProgressIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: isPerformingRequest ? 1.0 : 0.0,
child: new CircularProgressIndicator(),
),
),
);
}

最后一步是把组件添加到ListView中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: Text("Infinite ListView"),
),
body: ListView.builder(
itemCount: items.length + 1,
itemBuilder: (context, index) {
if (index == items.length) {
return _buildProgressIndicator();
} else {
return ListTile(title: new Text("Number $index"));
}
},
controller: _scrollController,
),
);
}

最终效果应该像这样:

infinite_dynamic_list_view_3

处理空数据

作为奖励,我将展示一个当没有数据从请求中返回时的简单处理方法。我们需要做的就是用ScrollController让我们的ListView来点动画:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
_getMoreData() async {
if (!isPerformingRequest) {
setState(() => isPerformingRequest = true);
List<int> newEntries = await fakeRequest(items.length, items.length); //returns empty list
if (newEntries.isEmpty) {
double edge = 50.0;
double offsetFromBottom = _scrollController.position.maxScrollExtent - _scrollController.position.pixels;
if (offsetFromBottom < edge) {
_scrollController.animateTo(
_scrollController.offset - (edge -offsetFromBottom),
duration: new Duration(milliseconds: 500),
curve: Curves.easeOut);
}
}
setState(() {
items.addAll(newEntries);
isPerformingRequest = false;
});
}
}

注意我们是如何检查在响应返回前用户没有向上滚动的。我们用offsetFromBottomedge进行比较还确定这件事。

infinite_dynamic_list_view_4

然后我们就完成了,亲爱的!🙂

这里可以找到包含了所有类的gist。

如果你对怎样更好的实现有任何问题或者建议,我强烈鼓励你留下评论。

干杯🙂

CATALOG
  1. 1. 起点
  2. 2. 动态数据加载
  3. 3. 进度指示器
  4. 4. 处理空数据