Posted at

Flutter の ListView / GridView で無限リストビュー

More than 1 year has passed since last update.

Flutter の ListView / GridView でいわゆる「無限リストビュー」を実装する方法を紹介します。アイテムを表示しつつ、末尾に到達したら次のアイテムを読み込んでいくやつです。

ListView / GridView には itemBuilder を受け取るコンストラクタがあります。

new ListView.builder(

itemBuilder: (BuildContext context, int index) {
// ここで index に応じた Widget を返す
return new Text(index.toString());
}
),

itemBuilder を使う場合は null を返したらリストビューの末尾であることを意味します。

つまり、以下のようにすることで、無限リストビューになります。


  • 要素があるうちは Widget を返す

  • 要素がなくなったら、次のアイテムを読み込む

  • 新しくアイテムを読み込んだら、setState で build させる

よって、おおよそ以下のような感じになります。

itemBuilder: (BuildContext context, int index) {

// List<String> items; というプロパティがあるとして
var length = items?.length ?? 0;

if (index == length) {
// アイテム数を超えたので次のページを読み込む
_load();

// 画面にはローディング表示しておく
return new Center(
child: new Container(
margin: const EdgeInsets.only(top: 8.0),
width: 32.0,
height: 32.0,
child: const CircularProgressIndicator(),
),
);
} else if (index > length) {
// ローディング表示より先は無し
return null;
}

// アイテムがあるので返す
return new Text(items[index]);
},

以下、コード全体。サンプルとして、GitHub の flutter/flutter の Issues をリスト表示しています。

import 'dart:async';

import 'dart:convert';

import 'package:flutter/material.dart';

import 'package:http/http.dart' as http;

void main() {
runApp(new MaterialApp(
home: new FooPage(),
));
}

class FooPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return new FooPageState();
}
}

class FooPageState extends State<FooPage> {
int page = 1;

List<String> issues;

bool loading = false;

@override
Widget build(BuildContext context) {
var length = issues?.length ?? 0;
return new Scaffold(
appBar: new AppBar(title: new Text("Infinite")),
body: new ListView.builder(
itemBuilder: (BuildContext context, int index) {
if (index == length) {
// アイテム数を超えたので次のページを読み込む
_load();

// 画面にはローディング表示しておく
return new Center(
child: new Container(
margin: const EdgeInsets.only(top: 8.0),
width: 32.0,
height: 32.0,
child: const CircularProgressIndicator(),
),
);
} else if (index > length) {
// ローディング表示より先は無し
return null;
}

// アイテムがあるので返す
var title = issues[index];
return new Container(
decoration: new BoxDecoration(
border: new Border(
bottom: new BorderSide(color: Colors.grey.shade300),
),
),
child: new ListTile(
key: new ValueKey<String>(title),
title: new Text(title),
),
);
},
),
);
}

Future<void> _load() async {
if (loading) {
return null;
}
loading = true;
try {
var url = "https://api.github.com/repositories/31792824/issues?page=${page}";
var resp = await http.get(url);
var data = json.decode(resp.body);
setState(() {
page += 1;
if (data is List) {
if (issues == null) {
issues = <String>[];
}
data.forEach((dynamic elem) {
if (elem is Map) {
issues.add(elem['title'] as String);
}
});
}
});
} finally {
loading = false;
}
}
}

出来ました。