Flutter
做爲時下最流行的技術之一,憑藉其出色的性能以及抹平多端的差別優點,早已引發大批技術愛好者的關注,甚至一些閒魚
,美團
,騰訊
等大公司均已投入生產使用。雖然目前其生態尚未徹底成熟,但身靠背後的Google
加持,其發展速度已經足夠驚人,能夠預見未來對Flutter
開發人員的需求也會隨之增加。前端
不管是爲了如今的技術嚐鮮仍是未來的潮流趨勢,都9102年了,做爲一個前端開發者,彷佛沒有理由不去嘗試它。正是帶着這樣的心理,筆者也開始學習Flutter
,同時建了一個用於練習的倉庫,後續全部代碼都會託管在上面,歡迎star,一塊兒學習。這是我寫的Flutter系列文章:git
在上一篇文章中,咱們學習了Flutter
中使用頻率最高的一些基礎組件。可是在一些場景中,當組件的寬度或高度超出屏幕邊緣時,Flutter
每每會給出overflow
警告,提醒有組件溢出屏幕。爲了解決這個問題,今天咱們就來學習最經常使用的一個滾動型容器組件
— ListView組件
。github
從功能比較上來看,Flutter
中的ListView
組件和RN
中的ScrollView
/FlatList
很是類似,可是在使用方法上仍是有點區別。接下來,就跟着我一塊兒來看看ListView
組件都有哪些經常使用的使用方法。segmentfault
第一種使用方法就是直接調用其默認構造函數
來建立列表,效果等同於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.separated
構造函數建立的列表,不在可視區域和預渲染區域內的子元素不會被建立或會被銷燬);children
: 容納子元素的組件數組。上面的屬性介紹一大堆,都不如一個實際例子來得實在。咱們能夠用一個ListView
組件來包裹上篇文章中實現的銀行卡,寵物卡片,朋友圈這三個例子:網絡
代碼(文件地址)app
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), ], ); } }
預覽less
能夠看到,默認構造函數
的用法很是之簡單,直接把子元素組件放在children
數組中就能夠了。可是潛在的問題前面也已經解釋過,對於長列表
這種應用場景仍是應該用ListView.build
構造函數性能會更好。ide
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
來建立再適合不過了。
絕大多數列表類的需求咱們均可以用ListView.build
構造函數來解決問題,不過有的列表子項之間須要分割線
,此時咱們能夠用Flutter
提供的另外一個構造函數ListView.separated
來建立列表。來看下其構造函數有什麼不一樣:
ListView.separated({ ... @required IndexedWidgetBuilder separatorBuilder })
相比於ListView.build
構造函數,能夠看到ListView.separated
僅僅是多了一個separatorBuilder
必填參數。顧名思義,這就是暴露給調用方自定義分割線組件的回調方法。以支付寶的好友列表爲例(好友卡片的樣式佈局能夠看這裏),咱們來看下ListView.separated
的使用方法:
代碼(文件地址)
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
這個函數,經過它咱們能夠自定義每一個子元素之間的分割線了。
到目前爲止,咱們一共學習了ListView
,ListView.build
和ListView.separated
三種建立列表的方式,它們各自都有其適用的場景,因此遇到需求時仍是得具體問題具體分析。
不過,其實ListView
還有一個構造函數:ListView.custom
。並且ListView.build
和ListView.separated
最終都是經過ListView.custom
實現的。可是本文並不打算介紹這種方法,由於通常狀況下前面提到的三種構造方法就已經足夠解決問題了(之後遇到實際問題再研究這個)。
上文咱們介紹了ListView
的基礎用法,可是在實際的產品中,咱們還會遇到列表下拉刷新
和上拉加載
等需求。接下來,就讓咱們學習下Flutter
中應該如何實現此類交互操做。
要在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
的源碼並非不少,等往後學了動畫再回頭來研究下如何定製一個自定義的下拉刷新組件。
除了下拉刷新以外,上拉加載是常常會遇到的另外一種列表操做。不過,此次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), ), ), ); } }
最後,咱們再來看下最終的實現效果:
首先,本文介紹了經常使用的ListView
,ListView.build
和ListView.separated
三種構造方法來建立列表,並結合實際的例子加以說明其不一樣的使用場景。緊接着,又介紹了列表組件下拉刷新
和上拉加載
這兩個較經常使用到的交互操做在Flutter
中應該如何實現。
經過文中的5個實際例子,相信你必定已經對Flutter
中如何使用ListView
有了初步瞭解,剩下的就是多練習(盤它)咯~