#はじめに
表題通り、FlutterとFirestoreで無限スクロールを作った覚書です。
アプリでリストを実装するときには、かなり利用する機会の多い実装かなと思いますが、
意外にハマることもあるので、何かの参考になればと思います。
#RefreshIndicatorの実装
オーバースクロールで画面を更新する、または任意の処理を実行する機能は、FlutterのRefreshIndicatorというウィジェットで定義されているため、まずはこのIndicatorがベースになります。
https://api.flutter.dev/flutter/material/RefreshIndicator-class.html
###やること1.RefreshIndicatorでListViewなどのスクローラブルな要素を包む
###やること2.onRefreshを定義する
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 => 現在の表示位置
@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を使って、区切って
スクロールされるごとに少しずつリストが読み込まれるようにします。
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];
}
#全体のコード
はこんな感じです。あくまで参考としてです。
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();
}
}
}
}