【Flutter】infinite_scroll_paginationを使った無限スクロールページネーションの実現

こんにちは、株式会社Pentagonでアプリ開発をしている鈴木です。

ページネーションの実装、みなさんどうやって実現していますか?
昨年弊社が公開したこちらの記事では、visibility_detectorというWidgetの可視性を判定してくれるパッケージを使って独自実装していました。

今回は、infinite_scroll_paginationというパッケージを使って、より簡単に無限スクロールページネーションを実装します。

【こんな人に読んで欲しい】

  • Flutterアプリ開発エンジニア
  • ページネーションの実装にかかる工数に頭を抱えている人

【この記事を読むメリット】

  • infinite_scroll_paginationで実現できる内容がわかる
  • ローコストにページネーションが実装できるようになる

【結論】
infinite_scroll_paginationは汎用性が高く、開発コストに貢献してくれるページネーションパッケージです。

目次

開発環境

  • Flutter v3.64.0
  • Dart v3.64.0
  • flutter_hooks: ^0.18.3
  • hooks_riverpod: ^1.0.4
  • vscode

パッケージ

パッケージにinfinite_scroll_paginationを追加します。

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  flutter_hooks: ^0.18.3
  hooks_riverpod: ^1.0.4
  cupertino_icons: ^1.0.2
  infinite_scroll_pagination: ^3.2.0

infinite_scroll_paginationとは

モバイルアプリの一覧画面ではスクロールが発生することがほとんどかと思います。その際一気に全データを取得すると、データの読み込みに時間がかかったり、画面構築にかかる処理が重くなったりするといった問題が発生します。
そこで、現状画面を表示するために必要なデータ(+いくつかのデータ)のみを取得して表示する、という処理を繰り返し行う必要が出てきます。
無限スクロールページネーション、自動ページネーション、遅延読み込みページネーションなどと呼ばれるものです。
これを簡単に実現してくれるのがinfinite_scroll_paginationパッケージとなっています。

Installing

infinite_scroll_pagination: ^3.2.0

をpubspec.yamlのdependenciesに追加し、flutter pub getをします。
あとは使いたいWidgetやViewのクラスでimportするだけです。

import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';

実際のコードを見ていきましょう。

pagination_test_widget.dart

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_pagination_sample/result.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter_pagination_sample/pagination_test_widget_model.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';

class PaginationTestWidget extends HookConsumerWidget {
  PaginationTestWidget({Key? key}) : super(key: key);

  // PagingControllerのコンストラクタの引数invisibleItemsThresholdに
  // 新しいページを要求する非表示アイテムの数を設定できる
  // デフォルトは3
final PagingController<int, Data> _pagingController =
      PagingController(firstPageKey: 0);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final viewModel = ref.watch(
      paginationTestWidgetModelProvider.notifier,
    );
    useEffect(
      () {
        _pagingController.addPageRequestListener((pageKey) {
          viewModel.fetchList(pageKey, (data) {
            // 最後のページかどうかで呼び出すメソッドを切り替える
            if (data.hasNextPage) {
              _pagingController.appendLastPage(data.list);
            } else {
              _pagingController.appendPage(data.list, data.currentPage + 1);
            }
          }, (error) {
            _pagingController.error = error;
          });
        });
        return () {
          _pagingController.dispose();
        };
      },
      const [],
    );

    return Scaffold(
      body: RefreshIndicator(
        onRefresh: () => Future.sync(
          () => _pagingController.refresh(),
        ),
        // ListView以外にGridViewにも対応している他、独自のレイアウトでも実装できる
        child: PagedListView.separated(
          pagingController: _pagingController,
          separatorBuilder: (context, index) => const Divider(),
          builderDelegate: PagedChildBuilderDelegate<Data>(
            itemBuilder: (context, item, index) =>
                ListTile(title: Text(item.number.toString())),
          ),
        ),
      ),
    );
  }
}

pagination_test_widget_model.dart

import 'package:flutter/material.dart';
import 'package:flutter_pagination_sample/result.dart';
import 'package:flutter_pagination_sample/repository.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

final paginationTestWidgetModelProvider =
    ChangeNotifierProvider.autoDispose((_) {
  return PaginationTestWidgetModel();
});

class PaginationTestWidgetModel extends ChangeNotifier {
  // データをリポジトリから取得する
  Future<void> fetchList(
    int page,
    void Function(Result) onSuccess,
    void Function(String) onError,
  ) async {
    try {
      final response = await Repository().getData(page);
      onSuccess(response);
    } catch (e) {
      onError('error');
    }
  }
}

main.dart

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter_pagination_sample/pagination_test_widget.dart';

void main() {
  runApp(
    // スコープの追加
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // 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(),
    );
  }
}

repository.dart

import 'package:flutter_pagination_sample/result.dart';

class Repository {
  // 0〜59までの60この数字を各ページのデータとして用意
  Future<Result> getData(int page) async {
    Result result;
    switch (page) {
      case 5:
        result = Result(
          List.generate(10, (i) => Data(10 * page + i)),
          page,
          true,
        );
        break;
      default:
        result = Result(
          List.generate(10, (i) => Data(10 * page + i)),
          page,
          false,
        );
        break;
    }
    return Future.delayed(
      const Duration(seconds: 2),
      () {
        return result;
      },
    );
  }
}

result.dart

class Result<T> {
  const Result(this.list, this.currentPage, this.hasNextPage);

  final List<Data> list;

  final int currentPage;

  final bool hasNextPage;
}

class Data {
  const Data(this.number);

  final int number;
}

参考リンク: https://qiita.com/derakudo/items/f991e6386e5ee764c0f7

まとめ

Infinite_scroll_paginationを使うと面倒な無限スクロールページネーションの実装が簡単に行えることがわかりました。
また、Infinite_scroll_paginationはListViewやGridViewだけでなく独自のレイアウトにも対応していたり、新しいページを要求する非表示アイテムの数も変更できたりと、カスタマイズの利く使いやすいパッケージです。

おまけ

Flutterに関する知識を深めたい方には、『Flutterの特徴・メリット・デメリットを徹底解説』という記事がおすすめです。

この記事では、Flutter アプリ開発の基本から、Flutter とは何か、そして実際のFlutter アプリ 事例を通じて、その将来性やメリット、デメリットまで詳しく解説しています。
Flutterを使ったアプリ開発に興味がある方、またはその潜在的な可能性を理解したい方にとって、必見の内容となっています。

ぜひ一度ご覧ください。

採用情報はこちら
目次