前言
最近Google開源的跨平臺移動開發框架Flutter非?;馃?,推出了1.0的正式版,趁著熱度,我也是抽空粗略地學習了一下。目前網上Flutter相關的資料和開源項目也非常多了,在學習的過程中給了我很多幫助。因此,我想通過一系列文章記錄一下自己學習Flutter遇到的一些問題,既是對自身技術的鞏固,也方便日后即時查閱。
本文介紹一下列表的下拉刷新和上拉加載,作為移動端最常見的場景之一,在Flutter中是怎樣實現的呢?
1.下拉刷新
和Android原生開發中的SwipeRefreshLayout效果相似,Flutter中也提供了一個Material風格的下拉刷新組件RefreshIndicator,用于實現下拉刷新功能。
構造方法如下:
const RefreshIndicator({
Key key,
@required this.child,
this.displacement = 40.0, // 下拉距離
@required this.onRefresh, // 刷新回調方法,返回類型必須為Future
this.color, // 刷新進度條顏色,默認當前主題顏色
this.backgroundColor, // 背景顏色
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
})
使用時,我們需要用RefreshIndicator去包裹ListView,并指定下拉刷新回調方法onRefresh,完整代碼如下:
import 'package:flutter/material.dart';
class ListViewPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _ListViewPageState();
}
}
class _ListViewPageState extends State<ListViewPage> {
// ListView數據集合
List<String> _list = List.generate(20, (i) => '原始數據${i + 1}');
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('列表的下拉刷新和上拉加載'),
),
body: Container(
child: RefreshIndicator(
child: ListView.builder(
itemBuilder: (context, index) => ListTile(
title: Text(_list[index]),
),
itemCount: _list.length,
),
onRefresh: _handleRefresh,
),
),
);
}
// 下拉刷新方法
Future<Null> _handleRefresh() async {
// 模擬數據的延遲加載
await Future.delayed(Duration(seconds: 2), () {
setState(() {
// 在列表開頭添加幾條數據
List<String> _refreshData = List.generate(5, (i) => '下拉刷新數據${i + 1}');
_list.insertAll(0, _refreshData);
});
});
}
}
這里定義了一個下拉刷新回調方法_handleRefresh(),每次下拉刷新都會調用該方法,在該方法中利用Future.delayed()
模擬網絡請求延遲加載數據。需要注意,該方法的返回值必須是Future類型。
// 下拉刷新方法
Future<Null> _handleRefresh() async {
// 模擬數據的延遲加載
await Future.delayed(Duration(seconds: 2), () {
setState(() {
// 在列表開頭添加幾條數據
List<String> _refreshData = List.generate(5, (i) => '下拉刷新數據${i + 1}');
_list.insertAll(0, _refreshData);
});
});
}
這樣,一個簡單的列表下拉刷新的效果就實現了。
我們在實際開發中有一種場景是:進入頁面自動請求數據并顯示加載進度圈,可以通過在根Widget中添加一個顯示加載進度的組件(比如ProgressIndicator),加載數據前后動態顯示和隱藏該組件來實現。但是既然我們已經使用了RefreshIndicator,可不可以直接利用它的下拉刷新進度圈呢?當然是可以的,這時候就需要利用RefreshIndicatorState了。
RefreshIndicator是一個StatefulWidget,它的State由RefreshIndicatorState管理,我們可以通過RefreshIndicatorState來改變RefreshIndicator的狀態,實現利用代碼動態顯示刷新進度圈。使用時需要使用GlobalKey對RefreshIndicatorState進行管理(我也不知道這樣說是否準確。。。),需要顯示刷新進度圈時再調用RefreshIndicatorState的
show()
方法即可,完整代碼如下:
import 'package:flutter/material.dart';
class ListViewPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _ListViewPageState();
}
}
class _ListViewPageState extends State<ListViewPage> {
// ListView數據集合
List<String> _list = new List();
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
GlobalKey<RefreshIndicatorState>();
@override
void initState() {
super.initState();
// 顯示加載進度圈
_showRefreshLoading();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('列表的下拉刷新和上拉加載'),
),
body: Container(
child: RefreshIndicator(
key: _refreshIndicatorKey,
child: ListView.builder(
itemBuilder: (context, index) => ListTile(
title: Text(_list[index]),
),
itemCount: _list.length,
),
onRefresh: _handleRefresh,
),
),
);
}
// 顯示加載進度圈
_showRefreshLoading() {
// 這里使用延時操作是由于在執行刷新操作時_refreshIndicatorKey還未與RefreshIndicator關聯
Future.delayed(const Duration(seconds: 0), () {
_refreshIndicatorKey.currentState.show();
});
}
// 下拉刷新方法
Future<Null> _handleRefresh() async {
// 模擬數據的延遲加載
await Future.delayed(Duration(seconds: 2), () {
setState(() {
// 在列表開頭添加幾條數據
List<String> _refreshData = List.generate(5, (i) => '下拉刷新數據${i + 1}');
_list.insertAll(0, _refreshData);
});
});
}
}
在initState()
中執行了_showRefreshLoading()方法,需要注意由于initState()是在build()之前調用的,此時_refreshIndicatorKey還沒有和Widget關聯,直接調用_refreshIndicatorKey.currentState.show()
會報錯。解決方法就是通過Future.delay,設置延遲時間為0,保證執行show()方法時RefreshIndicatorState已經被賦值。調用show()之后會自動調用onRefresh指定的回調方法_handleRefresh(),同時顯示加載進度圈,整個刷新效果看著還是比較自然的。
2.上拉加載
相比于下拉刷新,上拉加載的實現要相對麻煩一些。我查閱了一下網上的資料,實現上拉加載可以有兩種方式:第一種是通過指定ListView的controller屬性,類型是ScrollController,通過ScrollController可以判斷ListView是否滑動到了底部,再進行上拉加載的處理;第二種是利用NotificationListener,監聽ListVIew的滑動狀態,當ListView滑動到底部時,進行上拉加載處理。
方法一 利用ScrollController實現上拉加載更多
ListView有一個controller屬性,類型是ScrollController,通過ScrollController可以控制ListView的滑動狀態,判斷ListVIew是否滑動到了底部。判斷的方式如下:
ScrollController _scrollController;
@override
void initState() {
super.initState();
// 初始化ScrollController
_scrollController = ScrollController();
// 監聽ListView是否滾動到底部
_scrollController.addListener(() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent) {
// 滑動到了底部
print('滑動到了底部');
// 這里可以執行上拉加載邏輯
_loadMore();
}
});
}
@override
void dispose() {
super.dispose();
// 這里不要忘了將監聽移除
_scrollController.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('列表的下拉刷新和上拉加載'),
),
body: Container(
child: RefreshIndicator(
child: ListView.builder(
itemBuilder: (context, index) => ListTile(
title: Text(_list[index]),
),
itemCount: _list.length,
controller: _scrollController,
),
onRefresh: _handleRefresh,
),
),
);
}
_scrollController.position.pixels
表示ListView當前滑動的距離,_scrollController.position.maxScrollExtent
表示ListView可以滑動的最大距離,因此pixels >= maxScrollExtent
就表示ListView已經滑動到了底部,這時執行加載更多的邏輯即可,這里依然是用Future.delayed()
來模擬數據的延遲加載。當然不要忘記在dispose()方法中調用_scrollController.dispose()
來移除監聽,防止內存泄漏。
// 上拉加載
Future<Null> _loadMore() async {
// 模擬數據的延遲加載
await Future.delayed(Duration(seconds: 2), () {
setState(() {
List<String> _loadMoreData = List.generate(5, (i) => '上拉加載數據${i + 1}');
_list.addAll(_loadMoreData);
});
});
}
這里還有一個小問題,就是在加載數據的過程中,繼續上滑列表有可能會重復執行加載更多方法,為什么我說是有可能呢?加載更多方法是在滑動監聽中通過判斷執行的,也就是說如果我們在新數據還未加載出來時繼續上滑列表,如果沒有產生滑動偏移量,就不會執行addListener中的聲明的邏輯;但是如果上滑的過程中產生了偏移量(哈哈,說不定你手滑了呢),就會進入到監聽方法中,導致重復執行加載更多方法。解決這個問題的方法很簡單,我們只需要聲明一個變量isLoading來標識是否正在上拉加載就可以了,在加載數據前后更新isLoading的值。
bool isLoading = false; // 是否正在加載,防止多次請求加載下一頁
// 上拉加載
Future<Null> _loadMore() async {
if (!isLoading) {
setState(() {
isLoading = true;
});
// 模擬數據的延遲加載
await Future.delayed(Duration(seconds: 2), () {
setState(() {
isLoading = false;
List<String> _loadMoreData =
List.generate(5, (i) => '上拉加載數據${i + 1}');
_list.addAll(_loadMoreData);
});
});
}
}
這樣就實現了列表滑動到底部上拉加載更多數據的效果,目前還有一點需要優化的地方,一般我們在加載更多時會在ListView底部顯示一個加載進度圈,提示用戶此時正在加載數據。實現方法很簡單,就是為ListView添加一個Footer布局。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('列表的下拉刷新和上拉加載'),
),
body: Container(
child: RefreshIndicator(
child: ListView.builder(
itemBuilder: (context, index) {
if (index < _list.length) {
return ListTile(
title: Text(_list[index]),
);
} else {
// 最后一項,顯示加載更多布局
return _buildLoadMoreItem();
}
},
itemCount: _list.length + 1,
controller: _scrollController,
),
onRefresh: _handleRefresh,
),
),
);
}
// 加載更多布局
Widget _buildLoadMoreItem() {
return Center(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("加載中..."),
),
);
}
這里為了簡單,只用了一個Text提示用戶正在加載,實際開發中可以根據需求定制自己的加載布局。添加該布局的方法是將ListView的itemCount指定為_list.length + 1
,即添加一個item,然后itemBuilder中再根據index判斷是否為最后一項,返回相應的布局就行了。值得一提的是,其實這里也是簡單處理了,加載更多布局始終被添加到列表的最后一項,在實際應用中,我們需要根據具體情況來添加該布局。比如說,當數據集合為空或者數據全部加載完成后,就不需要顯示加載更多布局,還有一種情況是數據沒有填滿整個屏幕時,此時顯示加載更多布局就會很奇怪。
到這里,我們基本上就實現了列表的上拉加載更多。
完整的代碼如下:
import 'package:flutter/material.dart';
class ListViewPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _ListViewPageState();
}
}
class _ListViewPageState extends State<ListViewPage> {
// ListView數據集合
List<String> _list = List.generate(20, (i) => '原始數據${i + 1}');
ScrollController _scrollController;
bool isLoading = false; // 是否正在加載更多
@override
void initState() {
super.initState();
// 初始化ScrollController
_scrollController = ScrollController();
// 監聽ListView是否滾動到底部
_scrollController.addListener(() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent) {
// 滑動到了底部
print('滑動到了底部');
// 這里可以執行上拉加載邏輯
_loadMore();
}
});
}
@override
void dispose() {
super.dispose();
_scrollController.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('列表的下拉刷新和上拉加載'),
),
body: Container(
child: RefreshIndicator(
child: ListView.builder(
itemBuilder: (context, index) {
if (index < _list.length) {
return ListTile(
title: Text(_list[index]),
);
} else {
// 最后一項,顯示加載更多布局
return _buildLoadMoreItem();
}
},
itemCount: _list.length + 1,
controller: _scrollController,
),
onRefresh: _handleRefresh,
),
),
);
}
// 加載更多布局
Widget _buildLoadMoreItem() {
return Center(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("加載中..."),
),
);
}
// 下拉刷新方法
Future<Null> _handleRefresh() async {
// 模擬數據的延遲加載
await Future.delayed(Duration(seconds: 2), () {
setState(() {
// 在列表開頭添加幾條數據
List<String> _refreshData = List.generate(5, (i) => '下拉刷新數據${i + 1}');
_list.insertAll(0, _refreshData);
});
});
}
// 上拉加載
Future<Null> _loadMore() async {
if (!isLoading) {
setState(() {
isLoading = true;
});
// 模擬數據的延遲加載
await Future.delayed(Duration(seconds: 2), () {
setState(() {
isLoading = false;
List<String> _loadMoreData =
List.generate(5, (i) => '上拉加載數據${i + 1}');
_list.addAll(_loadMoreData);
});
});
}
}
}
方法二 利用NotificationListener實現上拉加載更多
NotificationListener是一個Widget,可以監聽子Widget發出的Notification。ListView在滑動的過程中會發出ScrollNotification類型的通知,我們可以通過監聽該通知得到ListView的滑動狀態,判斷是否滑動到了底部。NotificationListener有一個onNotification屬性,定義了監聽的回調方法,通過它來處理加載更多邏輯。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('列表的下拉刷新和上拉加載'),
),
body: Container(
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollNotification) {
if (scrollNotification.metrics.pixels >=
scrollNotification.metrics.maxScrollExtent) {
// 滑動到了底部
// 加載更多
_loadMore();
}
return false;
},
child: RefreshIndicator(
child: ListView.builder(
itemBuilder: (context, index) {
if (index < _list.length) {
return ListTile(
title: Text(_list[index]),
);
} else {
// 最后一項,顯示加載更多布局
return _buildLoadMoreItem();
}
},
itemCount: _list.length + 1,
),
onRefresh: _handleRefresh,
),
)),
);
}
判斷ListView是否滑動到底部的邏輯和方法一相同,依然是通過比較ListView當前滑動的距離和可以滑動的最大距離。加載更多的邏輯也和方法一是一樣的,這里就不多說了。
完整的代碼如下:
import 'package:flutter/material.dart';
class ListViewPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _ListViewPageState();
}
}
class _ListViewPageState extends State<ListViewPage> {
// ListView數據集合
List<String> _list = List.generate(20, (i) => '原始數據${i + 1}');
bool isLoading = false; // 是否正在加載更多
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('列表的下拉刷新和上拉加載'),
),
body: Container(
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollNotification) {
if (scrollNotification.metrics.pixels >=
scrollNotification.metrics.maxScrollExtent) {
// 滑動到了底部
// 加載更多
_loadMore();
}
return false;
},
child: RefreshIndicator(
child: ListView.builder(
itemBuilder: (context, index) {
if (index < _list.length) {
return ListTile(
title: Text(_list[index]),
);
} else {
// 最后一項,顯示加載更多布局
return _buildLoadMoreItem();
}
},
itemCount: _list.length + 1,
),
onRefresh: _handleRefresh,
),
)),
);
}
// 加載更多布局
Widget _buildLoadMoreItem() {
return Center(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("加載中..."),
),
);
}
// 下拉刷新方法
Future<Null> _handleRefresh() async {
// 模擬數據的延遲加載
await Future.delayed(Duration(seconds: 2), () {
setState(() {
// 在列表開頭添加幾條數據
List<String> _refreshData = List.generate(5, (i) => '下拉刷新數據${i + 1}');
_list.insertAll(0, _refreshData);
});
});
}
// 上拉加載
Future<Null> _loadMore() async {
if (!isLoading) {
setState(() {
isLoading = true;
});
// 模擬數據的延遲加載
await Future.delayed(Duration(seconds: 2), () {
setState(() {
isLoading = false;
List<String> _loadMoreData =
List.generate(5, (i) => '上拉加載數據${i + 1}');
_list.addAll(_loadMoreData);
});
});
}
}
}
總結
1.列表的下拉刷新是通過包裹一層RefreshIndicator,自定義onRefresh回調方法實現的
2.列表上拉加載的基本思路是監聽列表滑動狀態,當列表滑動到底部時,調用定義好的加載更多邏輯。監聽列表滑動狀態有兩種方式:ScrollController和NotificationListener,這兩種方式的實現差不多,選擇自己用得習慣的就好了,在使用ScrollController時要記得移除監聽。