封裝一個簡單的listview,下拉重新整理上拉載入
Getting Started
1.需求場景
在開發的過程中,經常要用到一個具有下拉重新整理和上拉載入更多功能的listview
,程式碼的實現思路基本是差不多的。所以有必要封裝一個通用的listview,方便使用。
2.需要用到的控制元件
- 下拉重新整理RefreshIndicator
- FutureBuilder:Flutter應用中的非同步模型,基於與Future互動的最新快照來構建自身的widget
- ScrollController,可以監聽listview的滑動狀態
- typedef:在Dart語言中,方法也是物件. 使用typedef,或者function-type alias來為方法型別命名,
然後可以使用命名的方法.當把方法型別賦值給一個變數的時候,typedef保留型別資訊.
具體使用方法:dart.goodev.org/guides/lang…
3.實現思路,佈局方式
目標:外部使用BaseListView的時候,只需要傳入一個頁面請求的操作和item構造的方法就可以使用。
1. 定義typedef
將頁面請求的方法定義為PageRequest,將構造子項的方法定義為ItemBuilder。
比如下面,PageRequest的返回值是列表資料的future,引數值是當前分頁和每頁頁數。在BaseListView中定義一個
PageRequest的變數給外面賦值,然後就可以通過變數呼叫外部的非同步操作。
ItemBuilder主要是提供給外部進行自定義構造子項,引數是資料來源list和當前位置position。
根據需要可以定義更多的typedef,這裡就只定義這兩個。
//型別定義
typedef Future<List<T>> PageRequest<T>(int page, int pageSize);
typedef Widget ItemBuilder<T>(List<T> list, int position);
複製程式碼
2. FutureBuilder+RefreshIndicator實現懶載入和下拉重新整理
這個之前已經實現過,可以看:github.com/LXD31256949…
3.利用ScrollController實現載入更多的功能
ListView中有一個ScrollController型別的引數,可以利用controller來監聽listview的滑動狀態,`
當滑動到底部的時候,可以loadmore操作
ListView({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
this.itemExtent,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
List<Widget> children = const <Widget>[],
int semanticChildCount,
})
複製程式碼
4. 一些預設的widget
- 底部的載入菊花:當在進行loadmore操作的時候,顯示底部的載入菊花,所以當在進行loadmore操作的時候,
list的長度要加1,然後把菊花這個item放到最後 - 載入資料出錯的狀態頁面,點選可以重試
- 載入資料為空的狀態頁面
4. 程式碼實現
/**這部分程式碼主要是設定滑動監聽,滑動到距離底部100單位的時候,開始進行loadmore操作
如果controller.position.pixels==controller.position.maxScrollExtent再去
進行loadmore操作的話,實際的顯示和操作會有點奇怪,所以這裡設定距離底部100
*/
controller = new ScrollController();
controller.addListener(() {
if (controller.position.pixels >=
controller.position.maxScrollExtent - 100) {
if (!isLoading) {
isLoading = true;
loadmore();
}
}
});
複製程式碼
/**
* 構造FutureBuilder
*/
FutureBuilder<List<T>> buildFutureBuilder() {
return new FutureBuilder<List<T>>(
builder: (context, AsyncSnapshot<List<T>> async) {
if (async.connectionState == ConnectionState.active ||
async.connectionState == ConnectionState.waiting) {
isLoading = true;
return new Center(
child: new CircularProgressIndicator(),
);
}
if (async.connectionState == ConnectionState.done) {
isLoading = false;
if (async.hasError) {
//有錯誤的時候
return new RetryItem(() {
refresh();
});
} else if (!async.hasData) {
//返回值為空的時候
return new EmptyItem(() {
refresh();
});
} else if (async.hasData) {
//如果是重新整理的操作
if (widget.page == 0) {
_list.addAll(async.data);
}
if (widget.total > 0 && widget.total <= _list.length) {
widget.enableLoadmore = false;
} else {
widget.enableLoadmore = true;
}
debugPrint(
"loadData hasData:page:${widget.page},pageSize:${widget.pageSize},list:${_list.length}");
//計算最終的list長度
int length = _list.length + (widget.hasHeader ? 1 : 0);
return new RefreshIndicator(
child: new ListView.separated(
physics: AlwaysScrollableScrollPhysics(),
controller: widget.enableLoadmore ? controller : null,
itemBuilder: (context, index) {
// TODO:頭部的更新,可能要放在外面,放在裡面的話也行,不過要封裝獲取頭部future的邏輯,然後提供一個外部builder給外部進行構造
// 目前需要在外面判斷position是否為0去構造頭部
// if (widget.hasHeader && index == 0 && widget.header != null) {
// return widget.header;
// }
//可以載入更多的時候,最後一個item顯示菊花
if (widget.enableLoadmore && index == length) {
return new LoadMoreItem();
}
return widget.itemBuilder(_list, index);
},
itemCount: length + (widget.enableLoadmore ? 1 : 0),
separatorBuilder: (BuildContext context, int index) {
return new Divider();
},
),
onRefresh: refresh);
}
}
},
future: future,
);
}
複製程式碼
下面是跟獲取資料有關的幾個方法:loadmore(),refresh(),loadData()。
loadData()會呼叫之前定義的頁面請求PageRequest方法
Future refresh() async {
debugPrint("loadData:refresh,list:${_list.length}");
if (!widget.enableRefresh) {
return;
}
if (isLoading) {
return;
}
_list.clear();
setState(() {
isLoading = true;
widget.page = 0;
future = loadData(widget.page, widget.pageSize);
futureBuilder = buildFutureBuilder();
});
}
void loadmore() async {
debugPrint("loadData:loadmore,list:${_list.length}");
loadData(++widget.page, widget.pageSize).then((List<T> data) {
setState(() {
isLoading = false;
_list.addAll(data);
futureBuilder = buildFutureBuilder();
});
});
}
Future<List<T>> loadData(int page, int pageSize) async {
debugPrint("loadData:page:$page,pageSize:$pageSize,list:${_list.length}");
return await widget.pageRequest(page, pageSize);
}
複製程式碼
5.注意的問題和踩坑
- 防止FutureBuilder進行不必要的重繪:這裡我採用的方法,是將getData()賦值給一個future的成員變數,
用它來儲存getData()的結果,以避免不必要的重繪
參考文章:blog.csdn.net/u011272795/… - FutureBuilder和RefreshIndicator的巢狀問題,到底誰是誰的child,這裡我是把RefreshIndicator作為FutureBuilder
的孩子。如果將RefreshIndicator放在外層,FutureBuilder作為child的話,當RefreshIndicator呼叫onrefreh重新整理資料並用
setState()去更新介面的時候,那FutureBuilder也會再次經歷生命週期,所以導致獲取資料的邏輯會被走兩遍
6.下一步TODO
- 存在的問題:當所有資料都已請求回來後,設定不能再載入更多,這個時候會多重新整理來一次頁面,暫時還未解決這個問題。
- 繼續完善這個Baselistview。
- 封裝另外一種Baselistview,用RefreshIndicator和NotificationListener來封裝就行。