riverpodとflutter_hooksで無限スクロール画面を作る

# 背景

[infinite_scroll_pagination](https://pub.dev/packages/infinite_scroll_pagination)というライブラリがあります。ページングの処理を隠蔽でき、page数やフェッチ処理のバリエーションを管理する必要がなくなったり、状況に応じたエラー画面を楽に実装できたり、色々便利で公開当初からお世話になってます。

基本的な利用方法は[example](https://github.com/EdsonBueno/infinite_scroll_pagination/tree/master/example)で公開されています。しかし[riverpod](https://pub.dev/packages/riverpod)と[flutter_hooks](https://pub.dev/packages/flutter_hooks)を利用したサンプルがなかったので、自分の実装を書いてみました。

<img width="300" alt="meodv-tvrtm.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168872/90d6b4ef-00c0-8d8b-87d0-20cf449b9760.gif">

# 実装

特有の処理はViewだけなので、Repository,ViewModelの実装は読み飛ばしても大丈夫です。

## Repository

データ返却用にRepositoryを用意しています。
ページングのAPIでよく見る`isLast`と`User`の配列を返却し、3ページ目が最後のページになるようにしています。


```dart:user_repository

  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を引数として、結果に応じて必要な関数を呼び出します。


```dart:users_view_model
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コードよりも若干スッキリしていると思います。

```dart:users_view
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のアーキテクチャガイド](https://developer.android.com/jetpack/guide?hl=ja)を参考にしています
- プロダクトでそのような実装をしたので`StateNotifier`や`StateNotifierProvider`のままサンプルコードに流用しました。状況に応じて`FutureProvider`を利用しても良いかもしれません。
    - 本来は検索機能などがあったためViewModelに色々実装しています。このコードでUsersViewStateを全く活用していないのもそういった事情です
- サンプルコードなのでエラー周りは手を抜いています

# 参考リンク
https://github.com/ham-burger/riverpod_with_infinite_scroll_pagination

https://pub.dev/packages/infinite_scroll_pagination

https://pub.dev/packages/riverpod

https://pub.dev/packages/flutter_hooks