1. 前言
Flutter
作爲時下最流行的技術之一,憑藉其出色的性能以及抹平多端的差異優勢,早已引起大批技術愛好者的關注,甚至一些閒魚
,美團
,騰訊
等大公司均已投入生產使用。雖然目前其生態還沒有完全成熟,但身靠背後的Google
加持,其發展速度已經足夠驚人,可以預見將來對Flutter
開發人員的需求也會隨之增長。
無論是爲了技術嚐鮮還是以後可能的工作機會,都9102年了,作爲一個前端開發者,似乎沒有理由不去嘗試它。正是帶着這樣的心理,筆者也開始學習Flutter
,同時建了一個用於練習的倉庫,後續所有代碼都會託管在上面,歡迎star,一起學習。
在上一篇文章中,我們學習了Flutter
中使用頻率最高的一些基礎組件。但是在一些場景中,當組件的寬度或高度超出屏幕邊緣時,Flutter
往往會給出overflow
警告,提醒有組件溢出屏幕。爲了解決這個問題,今天我們就來學習最常用的一個滾動型容器組件
— ListView組件
。
2. ListView使用方法
從功能比較上來看,Flutter
中的ListView
組件和RN
中的ScrollView
/FlatList
非常相似,但是在使用方法上還是有點區別。接下來,就跟着我一起來看看ListView
組件都有哪些常用的使用方法。
2.1 ListView()
第一種使用方法就是直接調用其默認構造函數
來創建列表,效果等同於RN
中的ScrollView
組件。但是這種方式創建的列表存在一個問題:對於那些長列表
或者需要較昂貴渲染開銷
的子組件,即使還沒有出現在屏幕中但仍然會被ListView
所創建,這將是一項較大的開銷,使用不當可能引起性能問題甚至卡頓。
不過話說回來,雖然該方法可能會有性能問題,但還是取決於其不同的應用場景而定。下面我們就來看下其構造函數(已省略不常用屬性):
ListView({
Axis scrollDirection = Axis.vertical,
ScrollController controller,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
this.itemExtent,
double cacheExtent,
List<Widget> children = const <Widget>[],
})
-
scrollDirection
: 列表的滾動方向,可選值有Axis
的horizontal
和vertical
,可以看到默認是垂直方向上滾動; -
controller
: 控制器,與列表滾動相關,比如監聽列表的滾動事件; -
physics
: 列表滾動至邊緣後繼續拖動的物理效果,Android
與iOS
效果不同。Android
會呈現出一個波紋狀(對應ClampingScrollPhysics
),而iOS
上有一個回彈的彈性效果(對應BouncingScrollPhysics
)。如果你想不同的平臺上呈現各自的效果可以使用AlwaysScrollableScrollPhysics
,它會根據不同平臺自動選用各自的物理效果。如果你想禁用在邊緣的拖動效果,那可以使用NeverScrollableScrollPhysics
; -
shrinkWrap
: 該屬性將決定列表的長度是否僅包裹其內容的長度。當ListView
嵌在一個無限長的容器組件中時,shrinkWrap
必須爲true,否則Flutter
會給出警告; -
padding
: 列表內邊距; -
itemExtent
: 子元素長度。當列表中的每一項長度是固定的情況下可以指定該值,有助於提高列表的性能(因爲它可以幫助ListView
在未實際渲染子元素之前就計算出每一項元素的位置); -
cacheExtent
: 預渲染區域長度,ListView
會在其可視區域的兩邊留一個cacheExtent
長度的區域作爲預渲染區域(對於ListView.build
或ListView.seperated
構造函數創建的列表,不在可視區域和預渲染區域內的子元素不會被創建或會被銷燬); -
children
: 容納子元素的組件數組。
上面的屬性介紹一大堆,都不如一個實際例子來得實在。我們可以用一個ListView
組件來包裹上篇文章中實現的銀行卡,寵物卡片,朋友圈這三個例子:
代碼(文件地址)
class NormalList extends StatelessWidget {
const NormalList({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
CreditCard(data: creditCardData),
PetCard(data: petCardData),
FriendCircle(data: friendCircleData),
],
);
}
}
預覽
可以看到,默認構造函數
的用法非常之簡單,直接把子元素組件放在children
數組中就可以了。但是潛在的問題前面也已經解釋過,對於長列表
這種應用場景還是應該用ListView.build
構造函數性能會更好。
2.2 ListView.build()
ListView
默認構造函數雖使用簡單,但不適用於長列表。爲此,我們來看下ListView.build
構造函數:
ListView.builder({
...
int itemCount,
@required IndexedWidgetBuilder itemBuilder,
})
這裏省略了不常用以及和ListView
默認構造函數重複的一些參數,相比之下我們可以發現ListView.builder
多了兩個新的參數:
-
itemCount
: 列表中元素的數量; -
itemBuilder
: 子元素的渲染方法,允許自定義子元素組件(等同於rn
中FlatList
組件的renderItem
屬性)。
不同於ListView
默認構造函數通過children
參數指定子元素的這種方式,ListView.build
通過暴露統一的itemBuilder
方法將渲染子元素的控制權交還給調用方。這裏我們用一個微信公衆號的例子來說明ListView.build
的使用方法(公衆號卡片的樣式佈局可以看這裏,也算是對基礎組件的一個鞏固和複習):
代碼(文件地址)
class SubscribeAccountList extends StatelessWidget {
const SubscribeAccountList({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: Color(0xFFEFEFEF),
child: ListView.builder(
itemCount: subscribeAccountList.length,
itemBuilder: (context, index) {
return SubscribeAccountCard(data: subscribeAccountList[index]);
},
),
);
}
}
預覽
根據上面的代碼可以看到,ListView.build
創建列表最重要的兩個參數就是itemCount
和itemBuilder
。對於公衆號列表這個例子,由於每個公衆號消息卡片的佈局都是有規則的,而且這個列表的數量可能非常之多,所以用ListView.build
來創建再適合不過了。
2.3 ListView.seperated()
絕大多數列表類的需求我們都可以用ListView.build
構造函數來解決問題,不過有的列表子項之間需要分割線
,此時我們可以用Flutter
提供的另一個構造函數ListView.seperated
來創建列表。來看下其構造函數有什麼不同:
ListView.separated({
...
@required IndexedWidgetBuilder separatorBuilder
})
相比於ListView.build
構造函數,可以看到ListView.seperated
僅僅是多了一個separatorBuilder
必填參數。顧名思義,這就是暴露給調用方自定義分割線組件的回調方法。以支付寶的好友列表爲例(好友卡片的樣式佈局可以看這裏),我們來看下ListView.seperated
的使用方法:
代碼(文件地址)
class FriendList extends StatelessWidget {
const FriendList({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.separated(
itemCount: friendListData.length,
itemBuilder: (context, index) {
return FriendCard(data: friendListData[index]);
},
separatorBuilder: (context, index) {
return Divider(
height: .5,
indent: 75,
color: Color(0xFFDDDDDD),
);
},
);
}
}
預覽
看代碼可以知道不同點就在於實現了separatorBuilder
這個函數,通過它我們可以自定義每個子元素之間的分割線了。
2.4 小結
到目前爲止,我們一共學習了ListView
,ListView.build
和ListView.seperated
三種創建列表的方式,它們各自都有其適用的場景,所以遇到需求時還是得具體問題具體分析。
不過,其實ListView
還有一個構造函數:ListView.custom
。而且ListView.build
和ListView.seperated
最終都是通過ListView.custom
實現的。但是本文並不打算介紹這種方法,因爲一般情況下前面提到的三種構造方法就已經足夠解決問題了(以後遇到實際問題再研究這個)。
3. ListView進階方法
上文我們介紹了ListView
的基礎用法,但是在實際的產品中,我們還會遇到列表下拉刷新
和上拉加載
等需求。接下來,就讓我們學習下Flutter
中應該如何實現此類交互操作。
3.1 下拉刷新
要在Flutter
中實現列表的下拉刷新效果,其實非常簡單,因爲Flutter
給我們封裝好了一個RefreshIndicator
組件,使用起來也非常方便。看下示例代碼:
class PullDownRefreshList extends StatefulWidget {
const PullDownRefreshList({Key key}) : super(key: key);
@override
_PullDownRefreshListState createState() => _PullDownRefreshListState();
}
class _PullDownRefreshListState extends State<PullDownRefreshList> {
Future onRefresh() {
return Future.delayed(Duration(seconds: 1), () {
Toast.show('當前已是最新數據', context);
});
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: this.onRefresh,
child: ListView.separated(
itemCount: friendListData.length,
itemBuilder: (context, index) {
return FriendCard(data: friendListData[index]);
},
separatorBuilder: (context, index) {
return Divider(
height: .5,
indent: 75,
color: Color(0xFFDDDDDD),
);
},
),
);
}
}
由於列表的數據源是可變的,因此這次的組件我們選用繼承自StatefulWidget
。
可以看到RefreshIndicator
的用法十分簡單,只要將我們原來的ListView
作爲其child,並且實現其onRefresh
方法就好了。而onRefresh
方法其實是刷新完畢通知RefreshIndicator
的一個回調函數。上述代碼中,我們模擬了一個1s的等待當做網絡請求,然後彈出一個Toast提示"已經是最新數據"(此處的Toast
是安裝了toast: ^0.1.3
這個包,Flutter
原生並沒有提供)。
這裏模仿了今日頭條的列表UI作爲示例(新聞卡片的樣式佈局可以看這裏),我們來看下效果:
可以看到一切都如預期成功執行了,效果還是不錯的,而且RefreshIndicator
使用起來也是非常簡便。但是,由於Flutter
封裝好的RefreshIndicator
組件可定製性有點弱,不太能夠滿足大多數app中自定義樣式的要求。不過好在看了下RefreshIndicator
的源碼並不是很多,等日後學了動畫再回頭來研究下如何定製一個自定義的下拉刷新組件。
3.2 上拉加載
除了下拉刷新之外,上拉加載是經常會遇到的另一種列表操作。不過,這次Flutter
倒是沒有像下拉刷新那樣提供現成的組件可以直接調用,上拉加載的交互需要我們自己完成。爲此,我們先來簡單分析下:
- 組件內部需要一個
list
變量存儲當前列表的數據源; - 組件內部需要一個
bool
型的isLoading
標誌位來表示當前是否處於Loading
狀態; - 需要能夠判斷出當前列表是否已經滾動到底部,而這就要藉助到我們前面提到過的
controller
屬性了(ScrollController
可以獲取到當前列表的滾動位置以及列表最大滾動區域,相比較即可得到結果); - 當開始加載數據的時候,需要將
isLoading
置爲true
;當數據加載完畢的時候,需要將新的數據合併到list
變量中,並且重新將isLoading
置爲false
。
根據上面的思路,我們可以得到下面的代碼:
class PullUpLoadMoreList extends StatefulWidget {
const PullUpLoadMoreList({Key key}) : super(key: key);
@override
_PullUpLoadMoreListState createState() => _PullUpLoadMoreListState();
}
class _PullUpLoadMoreListState extends State<PullUpLoadMoreList> {
bool isLoading = false;
ScrollController scrollController = ScrollController();
List<NewsViewModel> list = List.from(newsList);
@override
void initState() {
super.initState();
// 給列表滾動添加監聽
this.scrollController.addListener(() {
// 滑動到底部的關鍵判斷
if (
!this.isLoading &&
this.scrollController.position.pixels >= this.scrollController.position.maxScrollExtent
) {
// 開始加載數據
setState(() {
this.isLoading = true;
this.loadMoreData();
});
}
});
}
@override
void dispose() {
// 組件銷燬時,釋放資源(一定不能忘,否則可能會引起內存泄露)
super.dispose();
this.scrollController.dispose();
}
Future loadMoreData() {
return Future.delayed(Duration(seconds: 1), () {
setState(() {
this.isLoading = false;
this.list.addAll(newsList);
});
});
}
Widget renderBottom() {
// TODO
}
@override
Widget build(BuildContext context) {
return ListView.separated(
controller: this.scrollController,
itemCount: this.list.length + 1,
separatorBuilder: (context, index) {
return Divider(height: .5, color: Color(0xFFDDDDDD));
},
itemBuilder: (context, index) {
if (index < this.list.length) {
return NewsCard(data: this.list[index]);
} else {
return this.renderBottom();
}
},
);
}
}
其中有一點需要注意,列表的itemCount
值變成了list.length + 1
,這是因爲我們多渲染了一個底部組件
。當不在加載的時候,我們可以展示一個上拉加載更多
的提示性組件;當正在加載數據時,我們又可以展示一個努力加載中...
的佔位組件。renderBottom
的實現如下:
Widget renderBottom() {
if(this.isLoading) {
return Container(
padding: EdgeInsets.symmetric(vertical: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'努力加載中...',
style: TextStyle(
fontSize: 15,
color: Color(0xFF333333),
),
),
Padding(padding: EdgeInsets.only(left: 10)),
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 3),
),
],
),
);
} else {
return Container(
padding: EdgeInsets.symmetric(vertical: 15),
alignment: Alignment.center,
child: Text(
'上拉加載更多',
style: TextStyle(
fontSize: 15,
color: Color(0xFF333333),
),
),
);
}
}
最後,我們再來看下最終的實現效果:
4. 總結
首先,本文介紹了常用的ListView
,ListView.build
和ListView.seperated
三種構造方法來創建列表,並結合實際的例子加以說明其不同的使用場景。緊接着,又介紹了列表組件下拉刷新
和上拉加載
這兩個較常用到的交互操作在Flutter
中應該如何實現。
通過文中的5個實際例子,相信你一定已經對Flutter
中如何使用ListView
有了初步瞭解,剩下的就是多練習(盤它)咯~