对于REST API接口,大部分情况下都得处理分页问题。
分页可以让我们把大量数据分割成一个个小段,分次加载。这样可以有效避免因为一次load全部数据而导致客户端变慢的问题。
本文介绍Flutter ListView组件的分页实现,数据来源于HTTP请求。当用户下拉时,APP自动加载更多数据。
首先,要为 ListView 绑定为 Stateful 组件,以便能加载新数据。
ListView的状态(state)信息可以在组件创建时异步加载,也能在组件的生命周期里面用setState修改。
import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(), ); } } class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { String nextPage = "https://swapi.co/api/people"; ScrollController _scrollController = new ScrollController(); bool isLoading = false; List names = new List(); …. @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Pagination"), ), body: Container( child: _buildList(), ), resizeToAvoidBottomPadding: false, ); } }
这里,当我们下拉但是还未收到数据时,resizeToAvoidBottomPadding 可以保留出一个空位,loading效果就能显示在这个空位中。
为模拟实际场景,我们这里从API接口拉取数据,以让我们更好地理解Flutter的分页机制。
这里我们用公开的SWAPI接口,这个接口支持分页返回人名。
dependencies: # .... dio: 2.0.22 # check for latest version.
dio是Dart的Http客户端,支持拦截器、全局配置、表单数据、请求撤销、文件下载和超时等。
因为 SWAPI接口返回数据包含下一页URL,所以数据请求方法我们这样写:
ffinal dio = new Dio(); void _getMoreData() async { if (!isLoading) { setState(() { isLoading = true; }); final response = await dio.get(nextPage); nextPage = response.data['next']; setState(() { isLoading = false; names.addAll(response.data['results']); }); } }
在 _getMoreData 方法中,我们通过网络请求来获取列表的更多数据。
接着我们来看看 scrollController 组件。scrollController 是一个滑动监听组件,这里我们用来控制何时加载数据。
当我们滑动界面时,如果没有达到尾页,并且数据不是正在加载中状态,scrollController 就会检查当前视图所在的位置来决定是否加载更多数据。
@override void initState() { this._getMoreData(); super.initState(); _scrollController.addListener(() { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) { _getMoreData(); } }); } @override void dispose() { _scrollController.dispose(); super.dispose(); }
ListView 支持 scrollController 事件绑定,当用户在ListView中滑动时,会出发 scrollController 事件。
现在,当我们滑动到单个 ListView 底部时,_getMoreData() 会获取新数据,并保存到组件的state中。
在ListView builder中,我们使用一个技巧来让最后一行显示加载条(ProgressBar)。
Widget _buildProgressIndicator() { return new Padding( padding: const EdgeInsets.all(8.0), child: new Center( child: new Opacity( opacity: isLoading ? 1.0 : 0.0, child: new CircularProgressIndicator(), ), ), ); } Widget _buildList() { return ListView.builder( //+1 for progressbar itemCount: names.length + 1, itemBuilder: (BuildContext context, int index) { if (index == names.length) { return _buildProgressIndicator(); } else { return new ListTile( title: Text((names[index]['name'])), onTap: () { print(names[index]); }, ); } }, controller: _scrollController, ); }
import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(), ); } } class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { String nextPage = "https://swapi.co/api/people"; ScrollController _scrollController = new ScrollController(); bool isLoading = false; List names = new List(); final dio = new Dio(); void _getMoreData() async { if (!isLoading) { setState(() { isLoading = true; }); final response = await dio.get(nextPage); nextPage = response.data['next']; setState(() { isLoading = false; names.addAll(response.data['results']); }); } } @override void initState() { this._getMoreData(); super.initState(); _scrollController.addListener(() { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) { _getMoreData(); } }); } @override void dispose() { _scrollController.dispose(); super.dispose(); } Widget _buildProgressIndicator() { return new Padding( padding: const EdgeInsets.all(8.0), child: new Center( child: new Opacity( opacity: isLoading ? 1.0 : 00, child: new CircularProgressIndicator(), ), ), ); } Widget _buildList() { return ListView.builder( //+1 for progressbar itemCount: names.length + 1, itemBuilder: (BuildContext context, int index) { if (index == names.length) { return _buildProgressIndicator(); } else { return new ListTile( title: Text((names[index]['name'])), onTap: () { print(names[index]); }, ); } }, controller: _scrollController, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Pagination"), ), body: Container( child: _buildList(), ), resizeToAvoidBottomPadding: false, ); } }