Flutter 例項 - 載入更多的ListView

黃馬發表於2018-07-03

Dart4Flutter - 01 – 變數、型別和函式

Dart4Flutter – 02 –控制流 和異常

Dart4Flutter – 03 – 類和泛型

Dart4Flutter – 04 – 非同步和庫

Dart4Flutter - 拾遺01 - flutter-dart環境搭建

Dart4Flutter - 不可變性

Flutter入門 - 狀態管理

Flutter 入門例項1

Flutter 入門 - Container 屬性詳解

Flutter 入門-本地訪問-MethodChannel

Flutter 例項 - 載入更多的ListView

Flutter 例項 - 從本地到Flutter通訊 - Event Channels

Flutter 例項 - 載入更多的ListView

本教程完成一個有載入更多的ListView,最終效果如下圖所示:

Flutter 例項 - 載入更多的ListView

開始

首先我們只在列表中展示10個整數。

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<int> items = List.generate(10, (i) => i); // 產生資料

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: AppBar(
        title: Text("Infinite ListView"),
      ),
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(title: new Text("Number $index"));
        },
      ),
    );
  }
}
複製程式碼

動態新增資料

首先我們模擬一個http請求,假設我們通過傳遞from和to引數,然後返回他們之間的數。我們將新增延遲時間,這樣看起來更像是網路載入。具體程式碼如下所示:

/// from - 包括, to - 不包括
/// 通過這個模擬http請求
Future<List<int>> fakeRequest(int from, int to) async {
// 如果對Future不熟悉,可以參考 https://juejin.im/post/5b2c67a351882574a756f2eb
 return Future.delayed(Duration(seconds: 2), () {
   return List.generate(to - from, (i) => i + from);
 });
}
複製程式碼

當使用者將列表滾動到最底,我們將會呼叫上面的方法。為了監聽列表是否已經滾動到最底,最簡單的方式就是給列表新增一個ScrollController,當列表滾動的時候,就會發出一個請求。但是防止頻繁的傳送http請求,我們需要新增一個變數isPerformingRequest,表示是否有請求正在進行。只有當isPerformingRequest為false時,才能開始一個新的請求。

class _MyHomePageState extends State<MyHomePage> {
  List<int> items = List.generate(10, (i) => i);
  ScrollController _scrollController = new ScrollController();
  bool isPerformingRequest = false; // 是否有請求正在進行

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
        _getMoreData();
      }
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  _getMoreData() async {
    if (!isPerformingRequest) { // 判斷是否有請求正在執行
      setState(() => isPerformingRequest = true);
      List<int> newEntries = await fakeRequest(items.length, items.length + 10);
      setState(() {
        items.addAll(newEntries);
        isPerformingRequest = false;// 下一個請求可以開始了
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: AppBar(
        title: Text("Infinite ListView"),
      ),
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(title: new Text("Number $index"));
        },
        controller: _scrollController,
      ),
    );
  }
}
複製程式碼

如果你現在執行程式碼,你可以看資料可以動態的載入,但是這個距離我們最終想要的效果還有差距。我們需要新增一些提示,告知使用者資料正在載入中。

Flutter 例項 - 載入更多的ListView

進度提示

相關的元件是CircularProgressIndicator,他被Center、Opacity、Padding所包裹。我們將用Opacity元件控制CircularProgressIndicator的顯示,當請求正在進行中時顯示。整個元件的程式碼如下所示:

Widget _buildProgressIndicator() {
  return new Padding(
    padding: const EdgeInsets.all(8.0),
    child: new Center(
      child: new Opacity(
        opacity: isPerformingRequest ? 1.0 : 0.0,
        child: new CircularProgressIndicator(),
      ),
    ),
  );
}
複製程式碼

最後一件事是將這個元件新增到我們的ListView上:

@override
Widget build(BuildContext context) {
  return new Scaffold(
    appBar: AppBar(
      title: Text("Infinite ListView"),
    ),
    body: ListView.builder(
      itemCount: items.length + 1,
      itemBuilder: (context, index) {
        if (index == items.length) {
          return _buildProgressIndicator();
        } else {
          return ListTile(title: new Text("Number $index"));
        }
      },
      controller: _scrollController,
    ),
  );
}
複製程式碼

最終效果如下:

Flutter 例項 - 載入更多的ListView

處理空資料情況

下面是附加內容,處理當請求返回的資料為空時的晴空。我們通過ScrollControllerListView一些動畫。

_getMoreData() async {
  if (!isPerformingRequest) {
    setState(() => isPerformingRequest = true);
    List<int> newEntries = await fakeRequest(items.length, items.length); //returns empty list
    if (newEntries.isEmpty) {
      double edge = 50.0;
      double offsetFromBottom = _scrollController.position.maxScrollExtent - _scrollController.position.pixels;
      if (offsetFromBottom < edge) {
        _scrollController.animateTo(
            _scrollController.offset - (edge -offsetFromBottom),
            duration: new Duration(milliseconds: 500),
            curve: Curves.easeOut);
      }
    }
    setState(() {
      items.addAll(newEntries);
      isPerformingRequest = false;
    });
  }
}
複製程式碼

Flutter 例項 - 載入更多的ListView
下面是完整的程式碼:

import 'dart:async';

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      theme: new ThemeData(primarySwatch: Colors.blue),
      home: new MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<int> items = List.generate(10, (i) => i);
  ScrollController _scrollController = new ScrollController();
  bool isPerformingRequest = false;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      if (_scrollController.position.pixels ==
          _scrollController.position.maxScrollExtent) {
        _getMoreData();
      }
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  _getMoreData() async {
    if (!isPerformingRequest) {
      setState(() => isPerformingRequest = true);
      List<int> newEntries = await fakeRequest(
          items.length, items.length + 10); //returns empty list
      if (newEntries.isEmpty) {
        double edge = 50.0;
        double offsetFromBottom = _scrollController.position.maxScrollExtent -
            _scrollController.position.pixels;
        if (offsetFromBottom < edge) {
          _scrollController.animateTo(
              _scrollController.offset - (edge - offsetFromBottom),
              duration: new Duration(milliseconds: 500),
              curve: Curves.easeOut);
        }
      }
      setState(() {
        items.addAll(newEntries);
        isPerformingRequest = false;
      });
    }
  }

  Widget _buildProgressIndicator() {
    return new Padding(
      padding: const EdgeInsets.all(8.0),
      child: new Center(
        child: new Opacity(
          opacity: isPerformingRequest ? 1.0 : 0.0,
          child: new CircularProgressIndicator(),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: AppBar(
        title: Text("Infinite ListView"),
      ),
      body: ListView.builder(
        itemCount: items.length + 1,
        itemBuilder: (context, index) {
          if (index == items.length) {
            return _buildProgressIndicator();
          } else {
            return ListTile(title: new Text("Number $index"));
          }
        },
        controller: _scrollController,
      ),
    );
  }
}

/// from - inclusive, to - exclusive
Future<List<int>> fakeRequest(int from, int to) async {
  return Future.delayed(Duration(seconds: 2), () {
    return List.generate(to - from, (i) => i + from);
  });
}
複製程式碼

有任何問題,歡迎大家提問

參考

相關文章