LoginSignup
14
9

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-09-14

背景

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

基本的な利用方法はexampleで公開されています。しかしriverpodflutter_hooksを利用したサンプルがなかったので、自分の実装を書いてみました。

meodv-tvrtm.gif

実装

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

Repository

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

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

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

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のアーキテクチャガイドを参考にしています
  • プロダクトでそのような実装をしたのでStateNotifierStateNotifierProviderのままサンプルコードに流用しました。状況に応じてFutureProviderを利用しても良いかもしれません。
    • 本来は検索機能などがあったためViewModelに色々実装しています。このコードでUsersViewStateを全く活用していないのもそういった事情です
  • サンプルコードなのでエラー周りは手を抜いています

参考リンク

14
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
9