Flutter之封裝一個下拉刷新上拉加載的listview

封裝一個簡單的listview,下拉刷新上拉加載

Getting Started

1.需求場景

在開發的過程當中,常常要用到一個具備下拉刷新和上拉加載更多功能的listview ,代碼的實現思路基本是差很少的。因此有必要封裝一個通用的listview,方便使用。git

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構造的方法就可使用。github

1. 定義typedef

將頁面請求的方法定義爲PageRequest,將構造子項的方法定義爲ItemBuilder。 好比下面,PageRequest的返回值是列表數據的future,參數值是當前分頁和每頁頁數。在BaseListView中定義一個 PageRequest的變量給外面賦值,而後就能夠經過變量調用外部的異步操做。 ItemBuilder主要是提供給外部進行自定義構造子項,參數是數據源list和當前位置position。 根據須要能夠定義更多的typedef,這裏就只定義這兩個。bash

//類型定義
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操做async

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方法ide

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/…佈局

相關文章
相關標籤/搜索