背景
infinite_scroll_paginationというライブラリがあります。ページングの処理を隠蔽でき、page数やフェッチ処理のバリエーションを管理する必要がなくなったり、状況に応じたエラー画面を楽に実装できたり、色々便利で公開当初からお世話になってます。
基本的な利用方法はexampleで公開されています。しかしriverpodとflutter_hooksを利用したサンプルがなかったので、自分の実装を書いてみました。
実装
特有の処理はViewだけなので、Repository,ViewModelの実装は読み飛ばしても大丈夫です。
Repository
データ返却用にRepositoryを用意しています。
ページングのAPIでよく見るisLast
とUser
の配列を返却し、3ページ目が最後のページになるようにしています。
Future<UsersResponse> getUsers(int page) async {
UsersResponse result;
switch (page) {
case 2:
result = UsersResponse(
page: page,
isLast: true,
users: List.generate(
5,
(index) => _createUser(10 * page + index),
),
);
break;
default:
result = UsersResponse(
page: page,
isLast: false,
users: List.generate(
10,
(index) => _createUser(10 * page + index),
),
);
break;
}
return Future.delayed(
const Duration(seconds: 2),
() {
return result;
},
);
ViewModel
StateNotifier
を継承したViewModelを作成し、それをStateNotifierProvider
で返却するようにしています。
fetch処理ではページ数とonSuccess,onErrorを引数として、結果に応じて必要な関数を呼び出します。
final usersViewModelProvider =
StateNotifierProvider<UsersViewModel, UsersViewState>(
(ref) {
return UsersViewModel(
UsersViewState.initial(),
);
},
);
class UsersViewModel extends StateNotifier<UsersViewState> {
UsersViewModel(UsersViewState? usersViewState)
: super(usersViewState ?? UsersViewState.initial());
Future<void> fetchPage(
int page,
void Function(UsersResponse) onSuccess,
void Function(String) onError,
) async {
try {
final response = await UserRepository().getUsers(page);
onSuccess(response);
} catch (e) {
onError('error');
}
}
}
View
flutter_hooks
を利用して画面を作っています。paging_controller
の初期化と解放の処理をuse_effect
で捌くことで、元のexampleコードよりも若干スッキリしていると思います。
class UsersView extends HookConsumerWidget {
UsersView({super.key});
final PagingController<int, User> _pagingController =
PagingController(firstPageKey: 0);
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersViewModel = ref.watch(
usersViewModelProvider.notifier,
);
useEffect(
() {
_pagingController.addPageRequestListener((pageKey) {
usersViewModel.fetchPage(pageKey, (data) {
if (data.isLast) {
_pagingController.appendLastPage(data.users);
} else {
_pagingController.appendPage(data.users, data.nextPage);
}
}, (error) {
_pagingController.error = error;
});
});
return () {
_pagingController.dispose();
};
},
const [],
);
return RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView.separated(
pagingController: _pagingController,
separatorBuilder: (context, index) => const Divider(),
builderDelegate: PagedChildBuilderDelegate<User>(
itemBuilder: (context, item, index) => ListTile(
title: Text(item.id.toString()),
subtitle: Text(item.name),
),
),
),
);
}
}
その他
- アーキテクチャはAndroidのアーキテクチャガイドを参考にしています
- プロダクトでそのような実装をしたので
StateNotifier
やStateNotifierProvider
のままサンプルコードに流用しました。状況に応じてFutureProvider
を利用しても良いかもしれません。- 本来は検索機能などがあったためViewModelに色々実装しています。このコードでUsersViewStateを全く活用していないのもそういった事情です
- サンプルコードなのでエラー周りは手を抜いています
参考リンク