Flutter で無限ローディングを実装してみる、ただし有限
CREATED: 2023 / 02 / 19 Sun
UPDATED: 2023 / 08 / 04 Sun
まず StatefulWidget を用意
class InfiniteLoading extends StatefulWidget {
const InfiniteLoading({super.key});
@override
State<InfiniteLoading> createState() => _InfiniteLoadingState();
}
class _InfiniteLoadingState extends State<InfiniteLoading> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.cyan,
appBar: AppBar(
backgroundColor: Colors.redAccent,
),
);
}
}
適当に色をつけて画面を作りました。 ここから始めます。
API の用意
まず、ローディングするにはデータが必要ですね 今回は PokeAPI を使おうと思います。 https://pokeapi.co/
以下エンドポイントに GET リクエストを出すことでヒトカゲからポケモンの一覧を取得することができます。
https://pokeapi.co/api/v2/pokemon
レスポンスは以下のようになっていて、20件ごとに取得することができます。
{
"count": 1279,
"next": "https://pokeapi.co/api/v2/pokemon?offset=20&limit=20",
"previous": null,
"results": [
{
"name": "bulbasaur",
"url": "https://pokeapi.co/api/v2/pokemon/1/"
},
.
.
.
{
"name": "raticate",
"url": "https://pokeapi.co/api/v2/pokemon/20/"
}
]
}
next
に次のページを取得することができるエンドポイントが記載されているので、こちらを利用してどんどんデータを取得していきます。
http を利用したデータの取得
pubspec.yml
に http
を追加して早速 Flutter でデータを取得してみましょう。
dependencies:
flutter:
sdk: flutter
http: ^0.13.5
void _fetchPokemon() async {
http.Response _res = await http.get(
Uri.https('pokeapi.co', '/api/v2/pokemon'),
);
print(_res.body);
print(_res.statusCode);
}
// Scaffold の body に ElevatedButton を追加
Container(
width: double.infinity,
height: 100.0,
child: ElevatedButton(
onPressed: () {
_fetchPokemon();
},
child: Text('GET POKEMON'),
),
)
ElevatedButton
をタップするとポケモンのリストが以下のように表示されます。
flutter: {"count":1279,"next":"https://pokeapi.co/api/v2/pokemon?offset=20&limit=20","previous":null,"results":[{"name":"bulbasaur","url":"https://pokeapi.co/api/v2/pokemon/1/"},{"name":"ivysaur","url":"https://pokeapi.co/api/v2/pokemon/2/"},{"name":"venusaur","url":"https://pokeapi.co/api/v2/pokemon/3/"},{"name":"charmander","url":"https://pokeapi.co/api/v2/pokemon/4/"},{"name":"charmeleon","url":"https://pokeapi.co/api/v2/pokemon/5/"},{"name":"charizard","url":"https://pokeapi.co/api/v2/pokemon/6/"},{"name":"squirtle","url":"https://pokeapi.co/api/v2/pokemon/7/"},{"name":"wartortle","url":"https://pokeapi.co/api/v2/pokemon/8/"},{"name":"blastoise","url":"https://pokeapi.co/api/v2/pokemon/9/"},{"name":"caterpie","url":"https://pokeapi.co/api/v2/pokemon/10/"},{"name":"metapod","url":"https://pokeapi.co/api/v2/pokemon/11/"},{"name":"butterfree","url":"https://pokeapi.co/api/v2/pokemon/12/"},{"name":"weedle","url":"https://pokeapi.co/api/v2/pokemon/13/"},{"name":"kakuna","url":"https://pokeapi.co<…>
flutter: 200
このままではただの文字列のままなので、ウィジェット上でリスト化して扱うことができません。 なので、こちらを Pokemon のモデルに落とし込みます。
データモデルの作成
モデルはこんな感じにしておきます
class Pokemon {
String name;
Pokemon(this.name);
factory Pokemon.fromJson(Map<String, dynamic> json) {
final pokemon = Pokemon(json['name']);
return pokemon;
}
}
で、このモデルを利用して初期値をこしらえると、概ね以下のようになるかと思います。
_fetchPokemon
メソッドでは引数に uri
を取るようにして、次ページのリクエスト時にも柔軟に対応できるようにしています。
class _InfiniteLoadingState extends State<InfiniteLoading> {
List<Pokemon> _pokemons = [];
String? _nextPageLink;
void _fetchPokemon(Uri uri) async {
http.Response _res = await http.get(uri);
Map<String, dynamic> data = jsonDecode(_res.body);
List<dynamic> dd =
data['results'].map((item) => Pokemon.fromJson(item)).toList();
setState(() {
_pokemons.addAll(dd.map((item) => item as Pokemon).toList());
_nextPageLink = data['next'];
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.cyan,
appBar: AppBar(
backgroundColor: Colors.redAccent,
),
body: Container(
width: double.infinity,
height: 100.0,
child: ElevatedButton(
onPressed: () {
_fetchPokemon(Uri.https('pokeapi.co', '/api/v2/pokemon'));
},
child: Text('GET POKEMON'),
),
),
);
}
}
この画面で GET POKEMON のボタンをタップすると、_pokemons
変数の中に Pokemon モデルのデータが蓄積されていきます!
あと、次のページ参照のためのリンクも _nextPageLink
で残しておきましょう。
RefreshIndicator でリストのスワイプ時にロード機能をつける
次に、このデータたちを表示する ListView
を用意し、これを RefreshIndicator
で包み込みます。
_pokemons
が空の時は初回ロードボタンとして GET POKEMON のボタンを配置し、
空ではなくなった時には RefreshIndicator
でロードできるようにしています。
Scaffold(
body: _pokemons.isEmpty // _pokemons が空かどうか判定
? Container(
width: double.infinity,
height: 100.0,
child: ElevatedButton(
onPressed: () {
_fetchPokemon(Uri.https('pokeapi.co', '/api/v2/pokemon'));
},
child: Text('GET POKEMON'),
),
)
: RefreshIndicator(
onRefresh: () async {
// _nextPageLink がある時はそれを指定する
if (_nextPageLink != null) {
_fetchPokemon(Uri.parse(_nextPageLink!));
} else {
_fetchPokemon(Uri.https('pokeapi.co', '/api/v2/pokemon'));
}
},
child: ListView.builder(
itemCount: _pokemons.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(_pokemons[index].name),
tileColor: Colors.greenAccent,
);
},
),
),
)
これで画面上部から下に画面をスワイプすることで、画面下部にデータが追加されるようになりましたが、 どちらかというと、スワイプした箇所からデータが表示されるようになってくれた方が嬉しいですよね。
画面の上部にデータを追加していく方法
画面上部に追加していく方法については非常に簡単で、ListView.builder
の reverse
を true
に設定するだけで良いです。
ListView.builder(
reverse: true,
itemCount: _pokemons.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(_pokemons[index].name),
tileColor: Colors.greenAccent,
);
},
)
そうすると、Twitter のタイムラインのように、上へ上へとデータが追加されるようになります。
では、下へのデータ追加についてはどうすれば良いのでしょうか。
画面の下部にデータを追加していく方法
以下のように実装すれば実現することができます。 細部は調整してください。
class _InfiniteLoadingState extends State<InfiniteLoading> {
final ScrollController _controller = ScrollController(); // ListView 用の controller を初期化する
bool _isLoading = false;
List<Pokemon> _pokemons = [];
String? _nextPageLink;
@override
void initState() {
// 初期データを取得しておくことで、初回ロードボタンを省く
_fetchPokemon(Uri.https('pokeapi.co', '/api/v2/pokemon'));
_controller.addListener(() {
if (_controller.position.maxScrollExtent == _controller.offset) {
// ListView のスクロールが最下部に達するたびにこの処理がよばれる
_add();
}
});
super.initState();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _add() {
// ここにはスクロールが最下部に達した時にしか来ないので、_nextPageLink が必須
if (_nextPageLink != null) {
_fetchPokemon(Uri.parse(_nextPageLink!));
}
}
void _fetchPokemon(Uri uri) async {
// ローディングの状態を API リクエストの前後で記録
setState(() {
_isLoading = true;
});
http.Response _res = await http.get(uri);
Map<String, dynamic> data = jsonDecode(_res.body);
List<dynamic> dd =
data['results'].map((item) => Pokemon.fromJson(item)).toList();
setState(() {
_pokemons.addAll(dd.map((item) => item as Pokemon).toList());
_nextPageLink = data['next'];
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.cyan,
appBar: AppBar(
backgroundColor: Colors.redAccent,
),
body: ListView.builder(
controller: _controller,
itemCount: _pokemons.length + 1, // 画面下部に表示する Indicator の領域を確保
itemBuilder: (context, index) {
if (index + 1 == _pokemons.length + 1) { // リストの全ての要素の後、Indicator を表示する
return Padding(
padding: EdgeInsets.all(40.0),
child: _isLoading
? Center(
child: CircularProgressIndicator(),
)
: Container(),
);
}
return ListTile(
title: Text('${_pokemons[index].name}'),
tileColor: Colors.greenAccent,
);
},
),
);
}
}
このように実装することで、ListView
のスクロール最下部に達して API リクエストが始まると Indicator が一瞬表示され、
データが取得されるとページ下部にデータが追加されるようになります。
を仕舞い
ざっとこんなところで以上、無限ローディング実装でした!
ソースコードはこちらのリポジトリからも参照できます。 yutaro1204 / flutter_list_view_loading_demo
main.dart
の home
をそれぞれ切り替えて使ってみてください。
home: const DefaultInfiniteLoading(), // 下スワイプすると下に追加されるスクロール画面
home: const AscendingInfiniteLoading(), // 画面の上部にデータを追加していくスクロール画面
home: const DescendingInfiniteLoading(), // 画面の下部にデータを追加していくスクロール画面