Flutter如何實現下拉重新整理和上拉載入更多
https://blog.csdn.net/huangxiaoguo1/article/details/85603172
- 效果
- 下拉重新整理
如果實現下拉重新整理,必須藉助RefreshIndicator,在listview外面包裹一層RefreshIndicator,然後在RefreshIndicator裡面實現onRefresh方法。
body: movieList.length == 0 ? new Center(child: new CircularProgressIndicator()) : new RefreshIndicator( color: const Color(0xFF4483f6), //下拉重新整理 child: ListView.builder( itemCount: movieList.length + 1, itemBuilder: (context, index) { if (index == movieList.length) { return _buildProgressMoreIndicator(); } else { return renderRow(index, context); } }, controller: _controller, //指明控制器載入更多使用 ), onRefresh: _pullToRefresh, ),
onRefresh方法的實現_pullToRefresh,注意這裡必須使用async 不然報錯
/**
* 下拉重新整理,必須非同步async不然會報錯
*/
Future _pullToRefresh() async {
currentPage = 0;
movieList.clear();
loadMoreData();
return null;
}
非同步載入資料,注意:在Flutter中重新整理資料使用的是setState,不然無效,資料不會重新整理;資料的獲取需要使用[]取值,不能使用物件“ . ”的取值方法!
//載入列表資料
loadMoreData() async {
this.currentPage++;
var start = (currentPage - 1) * pageSize;
var url =
"https://api.douban.com/v2/movie/$movieType?start=$start&count=$pageSize";
Dio dio = new Dio();
Response response = await dio.get(url);
setState(() {
movieList.addAll(response.data["subjects"]);
totalSize = response.data["total"];
});
}
上拉載入更多
載入更多需要對ListView進行監聽,所以需要進行監聽器的設定,在State中進行監聽器的初始化。
//初始化滾動監聽器,載入更多使用
ScrollController _controller = new ScrollController();
在構造器中設定監聽
//固定寫法,初始化滾動監聽器,載入更多使用
_controller.addListener(() {
var maxScroll = _controller.position.maxScrollExtent;
var pixel = _controller.position.pixels;
if (maxScroll == pixel && movieList.length < totalSize) {
setState(() {
loadMoreText = "正在載入中...";
loadMoreTextStyle =
new TextStyle(color: const Color(0xFF4483f6), fontSize: 14.0);
});
loadMoreData();
} else {
setState(() {
loadMoreText = "沒有更多資料";
loadMoreTextStyle =
new TextStyle(color: const Color(0xFF999999), fontSize: 14.0);
});
}
});
在listView中新增監聽controller方法
自此,Flutter如何實現下拉重新整理和上拉載入更多完成…
- 整個列表頁面程式碼參考如下:
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:douban/pages/movie/movieDetail.dart';
class MovieList extends StatefulWidget {
String movieType;
//構造器傳遞資料(並且接收上個頁面傳遞的資料)
MovieList({Key key, this.movieType}) : super(key: key);
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return new MovieListState(movieType: this.movieType);
}
}
class MovieListState extends State<MovieList> {
String movieType;
String typeName;
List movieList = new List();
int currentPage = 0; //第一頁
int pageSize = 10; //頁容量
int totalSize = 0; //總條數
String loadMoreText = "沒有更多資料";
TextStyle loadMoreTextStyle =
new TextStyle(color: const Color(0xFF999999), fontSize: 14.0);
TextStyle titleStyle =
new TextStyle(color: const Color(0xFF757575), fontSize: 14.0);
//初始化滾動監聽器,載入更多使用
ScrollController _controller = new ScrollController();
/**
* 構造器接收(MovieList)資料
*/
MovieListState({Key key, this.movieType}) {
//固定寫法,初始化滾動監聽器,載入更多使用
_controller.addListener(() {
var maxScroll = _controller.position.maxScrollExtent;
var pixel = _controller.position.pixels;
if (maxScroll == pixel && movieList.length < totalSize) {
setState(() {
loadMoreText = "正在載入中...";
loadMoreTextStyle =
new TextStyle(color: const Color(0xFF4483f6), fontSize: 14.0);
});
loadMoreData();
} else {
setState(() {
loadMoreText = "沒有更多資料";
loadMoreTextStyle =
new TextStyle(color: const Color(0xFF999999), fontSize: 14.0);
});
}
});
}
//載入列表資料
loadMoreData() async {
this.currentPage++;
var start = (currentPage - 1) * pageSize;
var url =
"https://api.douban.com/v2/movie/$movieType?start=$start&count=$pageSize";
Dio dio = new Dio();
Response response = await dio.get(url);
setState(() {
movieList.addAll(response.data["subjects"]);
totalSize = response.data["total"];
});
}
@override
void initState() {
super.initState();
//設定當前導航欄的標題
switch (movieType) {
case "in_theaters":
typeName = "正在熱映";
break;
case "coming_soon":
typeName = "即將上映";
break;
case "top250":
typeName = "Top250";
break;
}
//載入第一頁資料
loadMoreData();
}
/**
* 下拉重新整理,必須非同步async不然會報錯
*/
Future _pullToRefresh() async {
currentPage = 0;
movieList.clear();
loadMoreData();
return null;
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
backgroundColor: Colors.white,
appBar: new AppBar(
leading: new IconButton(
icon: const Icon(Icons.arrow_back),
onPressed:null ,
),
title: new Text(typeName != null ? typeName : "正在載入中...",
style: new TextStyle(color: Colors.black)),
backgroundColor: Colors.white,
),
body: movieList.length == 0
? new Center(child: new CircularProgressIndicator())
: new RefreshIndicator(
color: const Color(0xFF4483f6),
//下拉重新整理
child: ListView.builder(
itemCount: movieList.length + 1,
itemBuilder: (context, index) {
if (index == movieList.length) {
return _buildProgressMoreIndicator();
} else {
return renderRow(index, context);
}
},
controller: _controller, //指明控制器載入更多使用
),
onRefresh: _pullToRefresh,
),
);
}
/**
* 載入更多進度條
*/
Widget _buildProgressMoreIndicator() {
return new Padding(
padding: const EdgeInsets.all(15.0),
child: new Center(
child: new Text(loadMoreText, style: loadMoreTextStyle),
),
);
}
/**
* 列表的ltem
*/
renderRow(index, context) {
var movie = movieList[index];
var id = movie["id"];
var title = movie["title"];
var type = movie["genres"].join("、");
var year = movie["year"];
var score = movie["rating"]["average"];
return new Container(
height: 200,
color: Colors.white,
child: new InkWell(
onTap: () {
Navigator.of(context).push(new MaterialPageRoute(
builder: (ctx) => new MovieDetail(movieId: id)));
},
child: new Column(
children: <Widget>[
new Container(
height: 199,
// color: Colors.blue,
child: new Row(
children: <Widget>[
new Container(
width: 120.0,
height: 180.0,
margin: const EdgeInsets.all(10.0),
child: Image.network(movie["images"]["small"]),
),
Expanded(
child: new Container(
height: 180.0,
margin: const EdgeInsets.all(12.0),
child: new Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Text(
"電影名稱:$title",
style: titleStyle,
overflow: TextOverflow.ellipsis,
),
new Text(
"電影型別:$type",
style: titleStyle,
overflow: TextOverflow.ellipsis,
),
new Text(
"上映年份:$year",
style: titleStyle,
overflow: TextOverflow.ellipsis,
),
new Text(
"豆瓣評分:$score",
style: titleStyle,
overflow: TextOverflow.ellipsis,
)
],
),
),
),
],
),
),
//分割線
new Divider(height: 1)
],
),
));
}
}
///////
import 'dart:async';
import 'package:VPN/base/state.dart';
import 'package:VPN/gql/api/index.dart';
import 'package:VPN/style.dart';
import 'package:VPN/utils/toast.dart';
import 'package:VPN/widgets/index.dart';
import 'cupertino_refresh.dart';
typedef Future<List<T>> PageFuture<T>(int pageIndex);
typedef Future RefreshFuture();
typedef Widget ItemBuilder<T>(BuildContext context, T entry, int index);
class RefreshController<T> {
final List<T> _items;
final bool _usePrimary;
ScrollController _scrollController;
final _itemChangeNotifier = ChangeNotifier();
final _refreshNotifier = ChangeNotifier();
RefreshController({bool usePrimaryScrollController = true, Iterable<T> items})
: _items = items?.toList() ?? [],
_usePrimary = usePrimaryScrollController ?? true,
_scrollController =
(usePrimaryScrollController ?? true) ? null : ScrollController();
void refresh() {
_scrollController?.jumpTo(0);
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
_refreshNotifier.notifyListeners();
}
void updateItems(bool func(List<T> items)) {
final changed = func(_items);
if (changed) {
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
_itemChangeNotifier.notifyListeners();
}
}
void jumpToTop() {
_scrollController?.jumpTo(0.0);
}
void dispose() {
if (!_usePrimary) {
_scrollController?.dispose();
}
_itemChangeNotifier.dispose();
}
void _didChangeDependencies(BuildContext context) {
if (_usePrimary) {
_scrollController = PrimaryScrollController.of(context);
}
}
}
class Refresh<T> extends StatefulWidget {
/// 分頁列表
const Refresh.pagination({
Key key,
@required this.controller,
this.pageSize = API.PAGE_SIZE,
@required this.pageFuture,
@required this.itemBuilder,
this.separatorBuilder,
this.initialRefresh = true,
this.header,
this.refreshIndicatorBackgroundColor,
this.refreshIndicatorColor,
}) : assert(controller != null),
assert(pageSize != null),
assert(pageFuture != null),
assert(itemBuilder != null),
this.refreshFuture = null,
this.children = null,
this.gridDelegate = null,
super(key: key);
/// 固定列表
const Refresh.fixed({
Key key,
@required this.controller,
@required this.refreshFuture,
@required this.children,
this.initialRefresh = true,
this.header,
this.refreshIndicatorBackgroundColor,
this.refreshIndicatorColor,
}) : assert(controller != null),
assert(refreshFuture != null),
assert(children != null),
this.pageSize = null,
this.pageFuture = null,
this.itemBuilder = null,
this.gridDelegate = null,
this.separatorBuilder = null,
super(key: key);
/// 分頁表格
const Refresh.paginationGrid({
Key key,
@required this.controller,
this.pageSize = 20,
@required this.pageFuture,
@required this.itemBuilder,
@required this.gridDelegate,
this.initialRefresh = true,
this.header,
this.refreshIndicatorBackgroundColor,
this.refreshIndicatorColor,
}) : assert(controller != null),
assert(pageFuture != null),
assert(itemBuilder != null),
this.refreshFuture = null,
this.children = null,
this.separatorBuilder = null,
super(key: key);
final RefreshController<T> controller;
final int pageSize;
final PageFuture<T> pageFuture;
final ItemBuilder<T> itemBuilder;
// 固定列表
final RefreshFuture refreshFuture;
final List<Widget> children;
// 表格
final SliverGridDelegate gridDelegate;
final IndexedWidgetBuilder separatorBuilder;
/// 是否在初始化的時候觸發一次重新整理
final bool initialRefresh;
final Widget header;
final Color refreshIndicatorBackgroundColor;
final Color refreshIndicatorColor;
@override
_RefreshState<T> createState() => _RefreshState<T>();
}
class _RefreshState<T> extends BaseState<Refresh> {
VoidCallback _itemChangedListener;
VoidCallback _refreshListener;
int _pageIndex; // 從 1 開始
ValueNotifier<bool> _loadingMore;
bool _noMoreData;
bool _refreshing;
@override
void initState() {
super.initState();
_pageIndex = -1; // -1 表示第一次載入
_loadingMore = ValueNotifier<bool>(false);
_noMoreData = true;
_refreshing = false;
_itemChangedListener = () {
if (mounted) {
setState(() {});
}
};
widget.controller._itemChangeNotifier.addListener(_itemChangedListener);
_refreshListener = () async {
if (mounted) {
setState(() {
_refreshing = true;
});
}
// 觸發重新整理
await _onRefresh();
if (mounted) {
setState(() {
_refreshing = false;
});
}
};
widget.controller._refreshNotifier.addListener(_refreshListener);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
widget.controller._didChangeDependencies(context);
}
@override
void didUpdateWidget(Refresh oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
oldWidget.controller?._itemChangeNotifier
?.removeListener(_itemChangedListener);
oldWidget.controller?._refreshNotifier?.removeListener(_refreshListener);
widget.controller?._itemChangeNotifier?.addListener(_itemChangedListener);
widget.controller?._refreshNotifier?.addListener(_refreshListener);
}
}
@override
void dispose() {
widget.controller._refreshNotifier.removeListener(_refreshListener);
widget.controller._itemChangeNotifier.removeListener(_itemChangedListener);
super.dispose();
}
@override
Widget build(BuildContext context) {
final bool isPagination = widget.itemBuilder != null;
final slivers = <Widget>[
CupertinoSliverRefreshControl(
refreshTriggerPullDistance: 80.0,
refreshIndicatorExtent: 60.0,
onRefresh: _onRefresh,
initialRefresh: widget.initialRefresh,
builder: (context, refreshState, pulledExtent,
refreshTriggerPullDistance, refreshIndicatorExtent) {
return SimpleRefreshIndicator(
refreshState: refreshState,
pulledExtent: pulledExtent,
refreshTriggerPullDistance: refreshTriggerPullDistance,
refreshIndicatorExtent: refreshIndicatorExtent,
backgroundColor:
widget.refreshIndicatorBackgroundColor ?? AppColors.transparent,
color: widget.refreshIndicatorColor,
);
},
),
];
// 頭部
if (widget.header != null) {
slivers.add(SliverToBoxAdapter(child: widget.header));
}
if ((isPagination && widget.controller._items.isEmpty && _noMoreData) ||
(!isPagination && widget.children.length == 0)) {
// 沒有資料
if (_pageIndex >= 0) {
slivers.add(_emptySliver());
}
} else {
// 有資料
slivers.add(widget.gridDelegate == null ? _listSliver() : _gridSliver());
if (isPagination && !_noMoreData) {
// 還有更多資料的話
slivers.add(_loadMoreSliver());
}
}
Widget w;
w = CustomScrollView(
physics: const BouncingScrollPhysics(
parent: const AlwaysScrollableScrollPhysics(),
),
controller: widget.controller._scrollController,
slivers: slivers,
);
w = Stack(
alignment: Alignment.center,
children: <Widget>[
w,
...(!_refreshing
? []
: [
Container(
constraints: BoxConstraints.loose(Size(200, 200)),
padding: EdgeInsets.all(24),
decoration: const ShapeDecoration(
color: AppColors.popupBackground,
shape: const RoundedRectangleBorder(
borderRadius: const BorderRadius.all(
const Radius.circular(20),
),
),
),
child: LoadingIndicator(),
),
]),
],
);
return w;
}
Widget _listSliver() {
ListView listView;
if (widget.children != null) {
listView = ListView(
children: widget.children,
);
} else if (widget.separatorBuilder == null) {
listView = ListView.builder(
itemCount: widget.controller._items.length,
itemBuilder: (BuildContext context, int index) {
return widget.itemBuilder(
context,
widget.controller._items[index],
index,
);
},
);
} else {
listView = ListView.separated(
itemCount: widget.controller._items.length,
itemBuilder: (BuildContext context, int index) {
return widget.itemBuilder(
context,
widget.controller._items[index],
index,
);
},
separatorBuilder: widget.separatorBuilder,
);
}
// 包裹成 Sliver
Widget w;
w = SliverList(
delegate: listView.childrenDelegate,
);
return w;
}
Widget _gridSliver() {
GridView gridView;
gridView = GridView.builder(
itemCount: widget.controller._items.length,
gridDelegate: widget.gridDelegate,
itemBuilder: (BuildContext context, int index) {
return widget.itemBuilder(
context,
widget.controller._items[index],
index,
);
},
);
// 包裹成 Sliver
Widget w;
w = SliverGrid(
delegate: gridView.childrenDelegate,
gridDelegate: gridView.gridDelegate,
);
return w;
}
Widget _emptySliver() {
Widget w;
w = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.info_outline,
size: 14,
color: AppColors.white70,
),
Text(
' 沒有記錄',
style: TextStyle(
color: AppColors.white70,
fontSize: 14,
),
),
],
);
w = Container(
width: double.infinity,
height: 60,
alignment: Alignment.center,
child: w,
);
w = IgnorePointer(child: w);
// 包裹成 Sliver
w = SliverToBoxAdapter(child: w);
return w;
}
Widget _loadMoreSliver() {
Widget w;
w = LoadMoreButton(
_loadingMore,
onTap: () {
_onLoadMore();
},
);
w = Container(
width: double.infinity,
height: 60,
alignment: Alignment.center,
child: w,
);
// 包裹成 Sliver
w = SliverToBoxAdapter(child: w);
return w;
}
Future _onRefresh() async {
final bool isPagination = widget.itemBuilder != null;
try {
await (isPagination ? _onLoadFirstPage : widget.refreshFuture)();
// 等待動畫結束再 refresh
while (isInTransition.value) {
await Future.delayed(Duration(milliseconds: 100));
}
} catch (e) {
if (mounted) {
showToast(e.toString());
}
}
}
/// 載入首頁資料
Future _onLoadFirstPage() async {
// 重置狀態
_pageIndex = 1;
_noMoreData = false;
final List<T> page = await widget.pageFuture(_pageIndex++);
widget.controller._items.clear();
if (page.length < widget.pageSize) {
_noMoreData = true;
}
widget.controller._items.addAll(page);
if (mounted) {
setState(() {});
}
}
/// 載入下一頁資料
Future _onLoadMore() async {
_loadingMore.value = true;
List<T> page;
try {
page = await widget.pageFuture(_pageIndex++);
} catch (_) {
_loadingMore.value = false;
return;
}
if (page.length < widget.pageSize) {
_noMoreData = true;
}
widget.controller._items.addAll(page);
_loadingMore.value = false;
if (mounted) {
setState(() {});
}
}
}
class LoadMoreButton extends StatefulWidget {
final ValueNotifier<bool> loadingMore;
final VoidCallback onTap;
const LoadMoreButton(this.loadingMore, {Key key, this.onTap})
: super(key: key);
@override
_LoadMoreButtonState createState() => _LoadMoreButtonState();
}
class _LoadMoreButtonState extends State<LoadMoreButton> {
VoidCallback _onStateChanged;
@override
void initState() {
super.initState();
_onStateChanged = () {
if (mounted) {
setState(() {});
}
};
widget.loadingMore?.addListener(_onStateChanged);
}
@override
void didUpdateWidget(LoadMoreButton oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.loadingMore != oldWidget.loadingMore) {
oldWidget.loadingMore?.removeListener(_onStateChanged);
widget.loadingMore?.addListener(_onStateChanged);
}
}
@override
void dispose() {
widget.loadingMore?.removeListener(_onStateChanged);
super.dispose();
}
@override
Widget build(BuildContext context) {
Widget w;
w = Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: widget.loadingMore?.value != true
? <Widget>[
Icon(
Icons.all_inclusive,
size: 14,
color: AppColors.white,
),
Text(
' 點選載入更多',
style: TextStyle(
color: AppColors.white,
fontSize: 14,
),
),
]
: [
LoadingIndicator(
size: 14,
beginColor: AppColors.white,
endColor: AppColors.white,
),
],
);
w = Button(
child: w,
onPressed: () {
widget.onTap?.call();
},
);
return w;
}
}