Flutter滾動型容器組件 - ListView篇

圖片描述

1. 前言

Flutter做爲時下最流行的技術之一,憑藉其出色的性能以及抹平多端的差別優點,早已引發大批技術愛好者的關注,甚至一些閒魚美團騰訊等大公司均已投入生產使用。雖然目前其生態尚未徹底成熟,但身靠背後的Google加持,其發展速度已經足夠驚人,能夠預見未來對Flutter開發人員的需求也會隨之增加。前端

不管是爲了如今的技術嚐鮮仍是未來的潮流趨勢,都9102年了,做爲一個前端開發者,彷佛沒有理由不去嘗試它。正是帶着這樣的心理,筆者也開始學習Flutter,同時建了一個用於練習的倉庫,後續全部代碼都會託管在上面,歡迎star,一塊兒學習。這是我寫的Flutter系列文章:git

在上一篇文章中,咱們學習了Flutter中使用頻率最高的一些基礎組件。可是在一些場景中,當組件的寬度或高度超出屏幕邊緣時,Flutter每每會給出overflow警告,提醒有組件溢出屏幕。爲了解決這個問題,今天咱們就來學習最經常使用的一個滾動型容器組件ListView組件github

2. ListView使用方法

從功能比較上來看,Flutter中的ListView組件和RN中的ScrollView/FlatList很是類似,可是在使用方法上仍是有點區別。接下來,就跟着我一塊兒來看看ListView組件都有哪些經常使用的使用方法。segmentfault

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: 列表的滾動方向,可選值有Axishorizontalvertical,能夠看到默認是垂直方向上滾動;
  • controller : 控制器,與列表滾動相關,好比監聽列表的滾動事件;
  • physics: 列表滾動至邊緣後繼續拖動的物理效果,AndroidiOS效果不一樣。Android會呈現出一個波紋狀(對應ClampingScrollPhysics),而iOS上有一個回彈的彈性效果(對應BouncingScrollPhysics)。若是你想不一樣的平臺上呈現各自的效果可使用AlwaysScrollableScrollPhysics,它會根據不一樣平臺自動選用各自的物理效果。若是你想禁用在邊緣的拖動效果,那可使用NeverScrollableScrollPhysics
  • shrinkWrap: 該屬性將決定列表的長度是否僅包裹其內容的長度。當ListView嵌在一個無限長的容器組件中時,shrinkWrap必須爲true,不然Flutter會給出警告;
  • padding: 列表內邊距;
  • itemExtent: 子元素長度。當列表中的每一項長度是固定的狀況下能夠指定該值,有助於提升列表的性能(由於它能夠幫助ListView在未實際渲染子元素以前就計算出每一項元素的位置);
  • cacheExtent: 預渲染區域長度,ListView會在其可視區域的兩邊留一個cacheExtent長度的區域做爲預渲染區域(對於ListView.buildListView.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

2.2 ListView.build()

ListView默認構造函數雖使用簡單,但不適用於長列表。爲此,咱們來看下ListView.build構造函數:

ListView.builder({
  ...
  int itemCount,
  @required IndexedWidgetBuilder itemBuilder,
})

這裏省略了不經常使用以及和ListView默認構造函數重複的一些參數,相比之下咱們能夠發現ListView.builder多了兩個新的參數:

  • itemCount: 列表中元素的數量;
  • itemBuilder: 子元素的渲染方法,容許自定義子元素組件(等同於rnFlatList組件的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]);
        },
      ),
    );
  }
}

預覽

builder_usage

根據上面的代碼能夠看到,ListView.build建立列表最重要的兩個參數就是itemCountitemBuilder。對於公衆號列表這個例子,因爲每一個公衆號消息卡片的佈局都是有規則的,並且這個列表的數量可能很是之多,因此用ListView.build來建立再適合不過了。

2.3 ListView.separated()

絕大多數列表類的需求咱們均可以用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),
        );
      },
    );
  }
}

預覽

separated_usage

看代碼能夠知道不一樣點就在於實現了separatorBuilder這個函數,經過它咱們能夠自定義每一個子元素之間的分割線了。

2.4 小結

到目前爲止,咱們一共學習了ListViewListView.buildListView.separated三種建立列表的方式,它們各自都有其適用的場景,因此遇到需求時仍是得具體問題具體分析。

不過,其實ListView還有一個構造函數:ListView.custom。並且ListView.buildListView.separated最終都是經過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做爲示例(新聞卡片的樣式佈局能夠看這裏),咱們來看下效果:

pull_down_refresh_usage

能夠看到一切都如預期成功執行了,效果仍是不錯的,並且RefreshIndicator使用起來也是很是簡便。可是,因爲Flutter封裝好的RefreshIndicator組件可定製性有點弱,不太可以知足大多數app中自定義樣式的要求。不過好在看了下RefreshIndicator的源碼並非不少,等往後學了動畫再回頭來研究下如何定製一個自定義的下拉刷新組件。

3.2 上拉加載

除了下拉刷新以外,上拉加載是常常會遇到的另外一種列表操做。不過,此次Flutter卻是沒有像下拉刷新那樣提供現成的組件能夠直接調用,上拉加載的交互須要咱們本身完成。爲此,咱們先來簡單分析下:

  1. 組件內部須要一個list變量存儲當前列表的數據源;
  2. 組件內部須要一個bool型的isLoading標誌位來表示當前是否處於Loading狀態;
  3. 須要可以判斷出當前列表是否已經滾動到底部,而這就要藉助到咱們前面提到過的controller屬性了(ScrollController能夠獲取到當前列表的滾動位置以及列表最大滾動區域,相比較便可獲得結果);
  4. 當開始加載數據的時候,須要將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),
        ),
      ),
    );
  }
}

最後,咱們再來看下最終的實現效果:

pull_up_load_more_usage

4. 總結

首先,本文介紹了經常使用的ListViewListView.buildListView.separated三種構造方法來建立列表,並結合實際的例子加以說明其不一樣的使用場景。緊接着,又介紹了列表組件下拉刷新上拉加載這兩個較經常使用到的交互操做在Flutter中應該如何實現。

經過文中的5個實際例子,相信你必定已經對Flutter中如何使用ListView有了初步瞭解,剩下的就是多練習(盤它)咯~

本文全部代碼託管在這兒,也能夠關注個人Blog

相關文章
相關標籤/搜索