Бесконечная лента - привычная часть многих приложений. Но реализация пагинации часто превращается в повторяющийся код. Хочется один раз собрать универсальный механизм и переиспользовать его в разных экранах. В на примере Riverpod рассказывается, как это сделать без сторонних пакетов.
В чем суть проблемы:
Готовые пакеты для пагинации существуют. Но многие из них устарели, используют устаревшие классы или предполагают, что номер страницы всегда число. Это не всегда так. Например в банковском приложении транзакции могут группироваться по датам. Тогда индекс страницы - это дата или строка, а не целое число.
Решение - построить собственный контроллер поверх AsyncNotifier. Он будет хранить текущий индекс, загружать первую страницу и добавлять следующую к уже полученным элементам.
Как устроен контроллер:
Контроллер принимает два типа: T для данных и I для индекса страницы. Ему нужны три вещи: начальный индекс, способ загрузки страницы и способ вычисления следующего индекса.
abstract class PaginationController<T, I> {
I get initialPage;
late I currentPage = initialPage;
@override
Future<void> loadNextPage() async {
final newState = await AsyncValue.guard(() async {
currentPage = nextPage(currentPage);
final elements = await loadPage(currentPage);
return [...?state.valueOrNull, ...elements];
});
state = newState;
}
}
Механизм берет на себя хранение текущего индекса, загрузку первой страницы и добавление новых элементов. Конкретному экрану остается только определить начальный индекс, запрос данных и следующий индекс.
Как выглядит использование:
class RepositoriesViewModel extends AsyncNotifier<List<Repository>>
with AsyncPaginationController<Repository, int> {
@override
int get initialPage => 1;
Когда пользователь прокручивает список до конца, вызывается загрузка следующей страницы.
Поиск и фильтрация:
Пагинация редко существует сама по себе. Обычно рядом есть поиск и фильтры. Их тоже можно встроить в контроллер.
Для этого определяется интерфейс, который хранит текущий фильтр и умеет его применять.
abstract class PaginationFilter<F> {
late F currentFilter;
Future<void> applyFilter(F filter);
}
Механизм реализует этот интерфейс поверх контроллера пагинации. При применении фильтра сбрасывается индекс и загружается первая страница с новыми параметрами.
Обновление жестом:
Свайп вниз для обновления тоже встраивается. Достаточно добавить метод, который сбрасывает индекс и загружает данные заново. А в интерфейсе обернуть список в RefreshIndicator.
Вывод:
Пагинация на Riverpod может быть переиспользуемой. Контроллер поверх AsyncNotifier хранит индекс, загружает страницы и добавляет новые элементы. Конкретный экран определяет только начальный индекс, запрос и следующий индекс.
Такой подход дает чистую архитектуру, поддержку нечисловых индексов и легкое расширение поиском, фильтрацией и обновлением жестом.