こんにちは、株式会社Pentagonでアプリ開発をしている石渡港です。
https://pentagon.tokyo
今回はPentagonで利用しているFlutterのPaginationListについてまとめました。
その結果、PaginationListについての整理ができて、より深く理解できるようになりました。
【この記事を読むメリット】
- Pentagonで利用しているFlutterのPaginationListが使えるようになります
【こんな方に参考にしていただきたい】
- シンプルなPaginationListを求めている人
【結論】
今回は弊社で利用しているPaginationListについて環境、解説、利用方法の3つにまとめました。
環境
Flutter 2.2.3
利用しているPackage
弊社で利用しているPaginationListの解説
ItemBuilder
リストで表示する内容を定義するためのエイリアスです。
PaginationListData
ページング機能のデータが付加されたリストデータのクラスです。
リスト、ページ番号、次のコンテンツがあるかないかを保持しています。
保持するリストのクラスはジェネリクスで定義しています。
PaginationListView
ページネーションを行うListViewのクラスです。
先ほど定義したlistData、itemBuilder、
indicator、separator、追加ロードを行う際のコールバック、paddingの定義を行えます。
pagination_list_view.dart
import 'package:flutter/material.dart';
import 'package:visibility_detector/visibility_detector.dart';
/// リスト要素のWidgetのBuilder
typedef ItemBuilder<T> = Widget Function(
BuildContext context,
int index,
T item,
);
/// 次のページの読み込みが要求されたときのコールバック
typedef OnLoadRequested = void Function(int nextPage);
/// ページング機能のデータが付加されたリストデータ
@immutable
class PaginationListData<T> {
const PaginationListData(this.list, this.currentPage,
{required this.hasNextPage});
/// リストデータ
/// (基本的に可変操作を行わないこと。)
final List<T> list;
/// 現在のページ番号
final int currentPage;
/// 次のページのコンテンツが存在するかどうか
final bool hasNextPage;
@override
int get hashCode =>
31 * (31 * list.hashCode + currentPage.hashCode) + hasNextPage.hashCode;
@override
bool operator ==(Object other) =>
other is PaginationListData &&
list == other.list &&
currentPage == other.currentPage &&
hasNextPage == other.hasNextPage;
}
/// ページング機能付きリスト
class PaginationListView<T> extends StatefulWidget {
const PaginationListView({
Key? key,
required this.listData,
required this.itemBuilder,
this.indicator =defaultIndicator,
this.separatorBuilder,
this.onLoadRequested,
this.padding = EdgeInsets.zero,
}) : super(key: key);
/// デフォルトのIndicator
static constdefaultIndicator= Padding(
padding: EdgeInsets.all(8),
child: Center(child: CircularProgressIndicator()),
);
final PaginationListData<T> listData;
final ItemBuilder<T> itemBuilder;
final Widget indicator;
final IndexedWidgetBuilder? separatorBuilder;
final OnLoadRequested? onLoadRequested;
final EdgeInsets padding;
@override
State<StatefulWidget> createState() => _PaginationListViewState<T>();
}
class _PaginationListViewState<T> extends State<PaginationListView<T>> {
// 最後に読み込み要求を掛けたリストデータのハッシュ値
// (読み込み要求のコールバックを重複して呼ばないようにするための記録用)
int? hash;
@override
Widget build(BuildContext context) {
final list = widget.listData.list;
final shouldShowIndicator = widget.listData.hasNextPage;
final totalCount = list.length + (shouldShowIndicator ? 1 : 0);
return ListView.separated(
padding: widget.padding,
itemCount: totalCount,
separatorBuilder: widget.separatorBuilder ?? (context, index) => SizedBox(),
itemBuilder: (context, index) {
if (index < list.length) {
return widget.itemBuilder(context, index, list[index]);
} else {
return VisibilityDetector(
key: UniqueKey(),
onVisibilityChanged: (info) {
// 可視部分が閾値(しきいち)を超えた場合、
// 読み込み要求のコールバックを重複呼び出しがないように呼び出す。
const threshold = 0.0;
if (info.visibleFraction > threshold) {
final currentHash = widget.listData.hashCode;
final shouldNotify = hash != currentHash;
if (shouldNotify) {
hash = currentHash;
final nextPage = widget.listData.currentPage + 1;
if (widget.onLoadRequested != null) {
widget.onLoadRequested!(nextPage);
}
}
}
},
child: widget.indicator,
);
}
},
);
}
}
利用方法
main.dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:pagination_test/pagination_test_widget.dart';
void main() {
runApp(
// スコープの追加
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Pagination Test',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: PaginationTestWidget(),
);
}
}
pagination_test_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:pagination_test/pagination_list_view.dart';
import 'package:pagination_test/pagination_test_widget_model.dart';
class PaginationTestWidget extends StatelessWidget {
const PaginationTestWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Stack(
children: [
HookBuilder(
builder: (context) {
// リストデータ
final listData = useProvider(paginationTestWidgetModelProvider
.select((value) => value.listData));
// ページネーションリスト、int型で待ち受ける
return PaginationListView<int>(
listData: listData,
// アイテムの生成
itemBuilder: (context, idx, value) {
return Text(value.toString());
},
// 次のページの読み込み
onLoadRequested: (nextPage) {
// ViewModelで処理を行う
context
.read(paginationTestWidgetModelProvider)
.fetchList(nextPage);
},
);
},
),
Align(
alignment: Alignment.bottomRight,
child: MaterialButton(
child: const Text('-1を最上部に追加'),
onPressed: () {
context
.read(paginationTestWidgetModelProvider)
.addDataAtTop();
},
),
),
],
),
),
);
}
}
pagination_test_widget_model.dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:pagination_test/pagination_list_view.dart';
final paginationTestWidgetModelProvider =
ChangeNotifierProvider.autoDispose((_) {
return PaginationTestWidgetModel();
});
class PaginationTestWidgetModel extends ChangeNotifier {
PaginationTestWidgetModel() {
_listData = PaginationListData<int>(List.generate(10, (i) => i), 1,
hasNextPage: true);
notifyListeners();
}
late PaginationListData<int> _listData;
PaginationListData<int> get listData => _listData;
Future<void> fetchList(int page) async {
_listData = PaginationListData<int>(
_listData.list + List.generate(10, (i) => i + 10 * (page - 1)), page,
hasNextPage: true);
notifyListeners();
}
Future<void> addDataAtTop() async {
_listData = PaginationListData<int>(
[-1] + _listData.list, _listData.currentPage,
hasNextPage: true);
notifyListeners();
}
}
動作
リストの末尾になると、onLoadRequested
が発火してPaginationTestWidgetModel
のfetchList
を呼び出すことで次のページのデータを作成できます。
また、最上部に-1
をタップすることで、最上部に-1を挿入できることがわかります。こちらを応用すると、SNSなどの投稿があるリストで、最新のデータをローカル上で追加することなどに利用できます。
末尾の場合、fetchする際に少し工夫が必要ですが、最上部と同じやりかたで表現することが可能です。
【まとめ】
- Pentagonで利用しているPaginationListの概要と実装方法がわかりました。
シンプルなFlutterのPaginationListを利用したい人の参考になれば幸いです。