Flutter之封裝一個下拉重新整理上拉載入的listview

入魔的冬瓜發表於2019-03-04

封裝一個簡單的listview,下拉重新整理上拉載入

Flutter之封裝一個下拉重新整理上拉載入的listview

Getting Started

1.需求場景

在開發的過程中,經常要用到一個具有下拉重新整理和上拉載入更多功能的listview
,程式碼的實現思路基本是差不多的。所以有必要封裝一個通用的listview,方便使用。

2.需要用到的控制元件

  1. 下拉重新整理RefreshIndicator
  2. FutureBuilder:Flutter應用中的非同步模型,基於與Future互動的最新快照來構建自身的widget
  3. ScrollController,可以監聽listview的滑動狀態
  4. 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.注意的問題和踩坑

  1. 防止FutureBuilder進行不必要的重繪:這裡我採用的方法,是將getData()賦值給一個future的成員變數,
    用它來儲存getData()的結果,以避免不必要的重繪
    參考文章:blog.csdn.net/u011272795/…
  2. FutureBuilder和RefreshIndicator的巢狀問題,到底誰是誰的child,這裡我是把RefreshIndicator作為FutureBuilder
    的孩子。如果將RefreshIndicator放在外層,FutureBuilder作為child的話,當RefreshIndicator呼叫onrefreh重新整理資料並用
    setState()去更新介面的時候,那FutureBuilder也會再次經歷生命週期,所以導致獲取資料的邏輯會被走兩遍

6.下一步TODO

  • 存在的問題:當所有資料都已請求回來後,設定不能再載入更多,這個時候會多重新整理來一次頁面,暫時還未解決這個問題。
  • 繼續完善這個Baselistview。
  • 封裝另外一種Baselistview,用RefreshIndicator和NotificationListener來封裝就行。

7.程式碼地址:

user-gold-cdn.xitu.io/2018/11/25/…

相關文章