Flutter x Firestore x 無限スクロール

#はじめに
表題通り、FlutterとFirestoreで無限スクロールを作った覚書です。
アプリでリストを実装するときには、かなり利用する機会の多い実装かなと思いますが、
意外にハマることもあるので、何かの参考になればと思います。

#RefreshIndicatorの実装

オーバースクロールで画面を更新する、または任意の処理を実行する機能は、FlutterのRefreshIndicatorというウィジェットで定義されているため、まずはこのIndicatorがベースになります。
https://api.flutter.dev/flutter/material/RefreshIndicator-class.html


###やること1.RefreshIndicatorでListViewなどのスクローラブルな要素を包む

###やること2.onRefreshを定義する


```main.dart

      body: RefreshIndicator(
        child: ListView.builder(
          controller: controller,
          itemCount: _data.length + 1,
          itemBuilder: (_, int index) {
            if (index < _data.length) {
              final DocumentSnapshot document = _data[index];
              return new Container(
                height: 200.0,
                child: new Text(document['title']),
              );
            }
          },
        ),
        onRefresh: () async {
          _data.clear();
          _lastVisible = null;
          await _getData();
        },
      )
```


#ScrollListenrの実装

スクロールされた箇所を検知して、データを読み込むようにします。

position.maxScrollExtent => ListView全体の下端位置
position.pixels => 現在の表示位置


```main.dart

  @override
  void initState() {
    controller = new ScrollController()..addListener(_scrollListener);
  }

  void _scrollListener() {
    if (!_isLoading) {
      if (controller.position.pixels == controller.position.maxScrollExtent) {
        setState(() => _isLoading = true);
        _getData();
      }
    }
  }

```

#Firebaseでデータを取得する

Firestoreでデータを取得します。orderByされたリストをstartAfterを使って、区切って
スクロールされるごとに少しずつリストが読み込まれるようにします。

```main.dart

    if (_lastVisible == null)
      data = await Firestore.instance
          .collection('projects')
          .orderBy('start_at', descending: true)
          .limit(4)
          .getDocuments();
    else
      data = await Firestore.instance
          .collection('projects')
          .orderBy('start_at', descending: true)
          .startAfter([_lastData['start_at']])
          .limit(4)
          .getDocuments();

    if (data != null && data.documents.length > 0) {
          _lastData = data.documents[data.documents.length - 1];
    }
```



#全体のコード

はこんな感じです。あくまで参考としてです。

```main.dart
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class Project extends StatefulWidget {
  Project({this.firestore});

  final Firestore firestore;

  @override
  _ProjectState createState() => _ProjectState();
}

class _ProjectState extends State<Project> {
  ScrollController controller;
  DocumentSnapshot _lastData;
  bool _isLoading;

  List<DocumentSnapshot> _data = new List<DocumentSnapshot>();
  final scaffoldKey = GlobalKey<ScaffoldState>();

  @override
  void initState() {
    controller = new ScrollController()..addListener(_scrollListener);
    super.initState();
    _isLoading = true;
    _getData();
  }

  Future<Null> _getData() async {
    QuerySnapshot data;
    if (_lastData == null)
      data = await Firestore.instance
          .collection('projects')
          .orderBy('start_at', descending: true)
          .limit(4)
          .getDocuments();
    else
      data = await Firestore.instance
          .collection('projects')
          .orderBy('start_at', descending: true)
          .startAfter([_lastData['start_at']])
          .limit(4)
          .getDocuments();

    if (data != null && data.documents.length > 0) {
      _lastData = data.documents[data.documents.length - 1];
      if (mounted) {
        setState(() {
          _isLoading = false;
          _data.addAll(data.documents);
        });
      }
    } else {
      setState(() => _isLoading = false);
      scaffoldKey.currentState?.showSnackBar(
        SnackBar(
          content: Text('データがありません.'),
        ),
      );
    }
    return null;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: scaffoldKey,
      body: RefreshIndicator(
        child: ListView.builder(
          controller: controller,
          itemCount: _data.length + 1,
          itemBuilder: (_, int index) {
            if (index < _data.length) {
              final DocumentSnapshot document = _data[index];
              return new Container(
                height: 200.0,
                child: new Text(document['title']),
              );
            }
          },
        ),
        onRefresh: () async {
          _data.clear();
          _lastData = null;
          await _getData();
        },
      ),
    );
  }

  void _scrollListener() {
    if (!_isLoading) {
      if (controller.position.pixels == controller.position.maxScrollExtent) {
        setState(() => _isLoading = true);
        _getData();
      }
    }
  }
}
```