Flutter listview下拉重新整理 上拉載入更多

__white發表於2021-08-04

這是我參與8月更文挑戰的第4天,活動詳情檢視:8月更文挑戰

下拉重新整理

在Flutter中系統已經為我們提供了google material design的重新整理功能 , 樣式與原生Android一樣. 我們可以使用RefreshIndicator元件來實現Flutter中的下拉重新整理,下面們還是先來看下如何使用吧

RefreshIndicator

構造方法:

 const RefreshIndicator({
    Key key,
    @required this.child,
    this.displacement: 40.0,      //觸發下拉重新整理的距離
    @required this.onRefresh,     //下拉回撥方法
    this.color,                   //進度指示器前景色 預設為系統主題色
    this.backgroundColor,         //背景色
    this.notificationPredicate: defaultScrollNotificationPredicate,
  })
複製程式碼

然後我們看一下效果以及實現方式: 這裡寫圖片描述

然後我們看一下程式碼:

class _MyHomePageState extends State<MyHomePage> {
  List list = new List(); //列表要展示的資料
 

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    getData();
  }

  /**
   * 初始化list資料 加延時模仿網路請求
   */
  Future getData() async {
    await Future.delayed(Duration(seconds: 2), () {
      setState(() {
        list = List.generate(15, (i) => '哈嘍,我是原始資料 $i');
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: new Text(widget.title),
      ),
      body: RefreshIndicator(
        onRefresh: _onRefresh,
        child: ListView.builder(
          itemBuilder: _renderRow,
          itemCount: list.length,
        ),
      ),
    );
  }

  Widget _renderRow(BuildContext context, int index) {
    return ListTile(
      title: Text(list[index]),
    );
  }

  /**
   * 下拉重新整理方法,為list重新賦值
   */
  Future<Null> _onRefresh() async {
    await Future.delayed(Duration(seconds: 3), () {
      print('refresh');
      setState(() {
        list = List.generate(20, (i) => '哈嘍,我是新重新整理的 $i');
      });
    });
  }
}
複製程式碼

程式碼不復雜,我們一步步分析: MyHomePage 只是返回一個State,這裡省略了. 首先body裡我們返回了一個RefreshIndicator,這個元件自帶下拉回撥,然後裡面我們包裹了一個listview, 然後使用List.generate()方法來建立了一個長度為15的List,並把List裡的值賦值給ListView Item中的ListTile。 下拉回撥onRefresh 我們返回了一個改變list的方法 . 在上面的程式碼中我們使用_onRefresh()方法來處理下拉重新整理的回撥

/**
   * 下拉重新整理方法,為list重新賦值
   */
  Future<Null> _onRefresh() async {
    await Future.delayed(Duration(seconds: 3), () {
      print('refresh');
      setState(() {
        list = List.generate(20, (i) => '哈嘍,我是新重新整理的 $i');
      });
    });
  }
複製程式碼

其中 Future.delayed()方法可以選擇延遲處理任務,這裡我們假設網路的延遲是3秒. 這樣一個簡單的下拉重新整理就實現了.

上拉載入更多

對於載入更多的元件在Flutter中是沒有提供的,所以在這裡我們就需要考慮如何實現的。

在ListView中有一個ScrollController屬性,它就是專門來控制ListView滑動事件,在這裡我們可以根據ListView的位置來判斷是否滑動到了底部來做載入更多的處理。

在這裡我們可以使用如下程式碼來判斷ListView 是否滑動到了底部

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    getData();
    _scrollController.addListener(() {
      if (_scrollController.position.pixels ==
          _scrollController.position.maxScrollExtent) {
        print('滑動到了最底部');
        _getMore();
      }
    });
  }
複製程式碼

_scrollController是我們初始化的ScrollController物件,通過監聽我們可以判斷現在的位置是否是最大的下滑位置來判斷是否下滑到了底部。

看一下程式碼和效果: 這裡寫圖片描述

class _MyHomePageState extends State<MyHomePage> {
  List list = new List(); //列表要展示的資料
  ScrollController _scrollController = ScrollController(); //listview的控制器
  int _page = 1; //載入的頁數
  bool isLoading = false; //是否正在載入資料

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    getData();
    _scrollController.addListener(() {
      if (_scrollController.position.pixels ==
          _scrollController.position.maxScrollExtent) {
        print('滑動到了最底部');
        _getMore();
      }
    });
  }

  /**
   * 初始化list資料 加延時模仿網路請求
   */
  Future getData() async {
    await Future.delayed(Duration(seconds: 2), () {
      setState(() {
        list = List.generate(15, (i) => '哈嘍,我是原始資料 $i');
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: new Text(widget.title),
      ),
      body: RefreshIndicator(
        onRefresh: _onRefresh,
        child: ListView.builder(
          itemBuilder: _renderRow,
          itemCount: list.length,
          controller: _scrollController,
        ),
      ),
      // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

  Widget _renderRow(BuildContext context, int index) {
    return ListTile(
      title: Text(list[index]),
    );
  }

  /**
   * 下拉重新整理方法,為list重新賦值
   */
  Future<Null> _onRefresh() async {
    await Future.delayed(Duration(seconds: 3), () {
      print('refresh');
      setState(() {
        list = List.generate(20, (i) => '哈嘍,我是新重新整理的 $i');
      });
    });
  }

  /**
   * 上拉載入更多
   */
  Future _getMore() async {
    if (!isLoading) {
      setState(() {
        isLoading = true;
      });
      await Future.delayed(Duration(seconds: 1), () {
        print('載入更多');
        setState(() {
          list.addAll(List.generate(5, (i) => '第$_page次上拉來的資料'));
          _page++;
          isLoading = false;
        });
      });
    }
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    _scrollController.dispose();
  }
}
複製程式碼

滑動到底部的時候,我們執行載入更多的方法,給list資料多加5條,這次我們把延遲改到了1秒:

/**
   * 上拉載入更多
   */
  Future _getMore() async {
    if (!isLoading) {
      setState(() {
        isLoading = true;
      });
      await Future.delayed(Duration(seconds: 1), () {
        print('載入更多');
        setState(() {
          list.addAll(List.generate(5, (i) => '第$_page次上拉來的資料'));
          _page++;
          isLoading = false;
        });
      });
    }
  }
複製程式碼

是的,看著上面的效果我們已經實現了下拉載入更多,但是因為我們是滑動到底部觸發的,如果在正在請求的過程中多次下拉就會造成多次載入更多的情況,所以我們還得對這個做下處理為了避免多次觸發,我們加了一個isLoading,在上拉方法執行的過程中不會再次執行. 可以看到,我們僅僅在上面程式碼的基礎上加上了一個isLoading的變數,當這個變數的值為true時,就不會觸發載入更多的操作。

而因為是網路請求,可能需要分頁,所以我們加了個page引數來檢視是第幾次觸發上拉載入.

因為我們加了個監聽,在元件解除安裝掉的時候記得移除這個監聽,所以:

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    _scrollController.dispose();
  }
複製程式碼

這個一定不要忘記,養成好習慣,每次加了監聽都跑到這個方法裡移除掉.

這樣,我們一個簡單的上拉載入更多的功能就實現了. 但是還有個問題,沒有使用者互動啊,載入的時候要有個提示,於是我們嘗試上拉的時候展示一個載入中的元件給使用者: 首先我們建立載入更多時顯示的Vidget

/**
   * 載入更多時顯示的元件,給使用者提示
   */
  Widget _getMoreWidget() {
    return Center(
      child: Padding(
        padding: EdgeInsets.all(10.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Text(
              '載入中...     ',
              style: TextStyle(fontSize: 16.0),
            ),
            CircularProgressIndicator(strokeWidth: 1.0,)
          ],
        ),
      ),
    );
  }

複製程式碼

然後我們在listview的itemcount那裡把count+1,相當於我們給listview加了個尾部的元件.

 body: RefreshIndicator(
        onRefresh: _onRefresh,
        child: ListView.builder(
          itemBuilder: _renderRow,
          itemCount: list.length + 1,   //這裡!這裡!這裡!
          controller: _scrollController,
        ),
複製程式碼

看一下效果是否滿意: 這裡寫圖片描述

嗯,基本符合要求,感覺那個重新整理圖示加的有點醜,畫蛇添足了,不過功能都是ok了的. 當然, 大家可以根據自己的需要去自己實現想要的樣式 看一下全部的程式碼:

/*
 * Created by 李卓原 on 2018/9/13.
 * email: zhuoyuan93@gmail.com
 *
 */
 
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List list = new List(); //列表要展示的資料
  ScrollController _scrollController = ScrollController(); //listview的控制器
  int _page = 1; //載入的頁數
  bool isLoading = false; //是否正在載入資料

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    getData();
    _scrollController.addListener(() {
      if (_scrollController.position.pixels ==
          _scrollController.position.maxScrollExtent) {
        print('滑動到了最底部');
        _getMore();
      }
    });
  }

  /**
   * 初始化list資料 加延時模仿網路請求
   */
  Future getData() async {
    await Future.delayed(Duration(seconds: 2), () {
      setState(() {
        list = List.generate(15, (i) => '哈嘍,我是原始資料 $i');
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: new Text(widget.title),
      ),
      body: RefreshIndicator(
        onRefresh: _onRefresh,
        child: ListView.builder(
          itemBuilder: _renderRow,
          itemCount: list.length + 1,
          controller: _scrollController,
        ),
      ),
      // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

  Widget _renderRow(BuildContext context, int index) {
    if (index < list.length) {
      return ListTile(
        title: Text(list[index]),
      );
    }
    return _getMoreWidget();
  }

  /**
   * 下拉重新整理方法,為list重新賦值
   */
  Future<Null> _onRefresh() async {
    await Future.delayed(Duration(seconds: 3), () {
      print('refresh');
      setState(() {
        list = List.generate(20, (i) => '哈嘍,我是新重新整理的 $i');
      });
    });
  }

  /**
   * 上拉載入更多
   */
  Future _getMore() async {
    if (!isLoading) {
      setState(() {
        isLoading = true;
      });
      await Future.delayed(Duration(seconds: 1), () {
        print('載入更多');
        setState(() {
          list.addAll(List.generate(5, (i) => '第$_page次上拉來的資料'));
          _page++;
          isLoading = false;
        });
      });
    }
  }

  /**
   * 載入更多時顯示的元件,給使用者提示
   */
  Widget _getMoreWidget() {
    return Center(
      child: Padding(
        padding: EdgeInsets.all(10.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Text(
              '載入中...',
              style: TextStyle(fontSize: 16.0),
            ),
            CircularProgressIndicator(
              strokeWidth: 1.0,
            )
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    _scrollController.dispose();
  }
}

複製程式碼

總結:

  • RefreshIndicator可以顯示下拉重新整理

  • 使用ScrollController可以監聽滑動事件,判斷當前view所處的位置

  • 可以根據item所處的位置來處理載入更多顯示效果

相關文章