Добавить объявление

Переиспользуемая пагинация во Flutter на Riverpod

Бесконечная лента - привычная часть многих приложений. Но реализация пагинации часто превращается в повторяющийся код. Хочется один раз собрать универсальный механизм и переиспользовать его в разных экранах. В статье на примере Riverpod рассказывается, как это сделать без сторонних пакетов.

В чем суть проблемы:


Готовые пакеты для пагинации существуют. Но многие из них устарели, используют устаревшие классы или предполагают, что номер страницы всегда число. Это не всегда так. Например в банковском приложении транзакции могут группироваться по датам. Тогда индекс страницы - это дата или строка, а не целое число.

Решение - построить собственный контроллер поверх AsyncNotifier. Он будет хранить текущий индекс, загружать первую страницу и добавлять следующую к уже полученным элементам.

Как устроен контроллер:


Контроллер принимает два типа: T для данных и I для индекса страницы. Ему нужны три вещи: начальный индекс, способ загрузки страницы и способ вычисления следующего индекса.

abstract class PaginationController<T, I> {
I get initialPage;
late I currentPage = initialPage;

FutureOr<List<T>> loadPage(I page);
Future<void> loadNextPage();

I nextPage(I currentPage);
}

Затем создается механизм, который реализует этот интерфейс поверх AsyncNotifier.

mixin AsyncPaginationController<T, I> on AsyncNotifier<List<T>>
implements PaginationController<T, I> {
@override
late I currentPage = initialPage;

@override
FutureOr<List<T>> build() async => loadPage(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;

@override
FutureOr<List<Repository>> loadPage(int page) async {
final (_, items) = await repository.fetchRepositories(page: page);
return items;
}

@override
int nextPage(int currentPage) => currentPage + 1;
}

Все, что требуется от экрана - задать три параметра.

Как подключить в интерфейсе:


Осталось отслеживать конец прокрутки. Для этого используется NotificationListener.

NotificationListener(
onNotification: (ScrollNotification info) {
if (info is ScrollEndNotification &&
info.metrics.pixels >= info.metrics.maxScrollExtent) {
viewModel.loadNextPage();
}
return true;
},
child: CustomScrollView(...),
)

Когда пользователь прокручивает список до конца, вызывается загрузка следующей страницы.

Поиск и фильтрация:


Пагинация редко существует сама по себе. Обычно рядом есть поиск и фильтры. Их тоже можно встроить в контроллер.

Для этого определяется интерфейс, который хранит текущий фильтр и умеет его применять.

abstract class PaginationFilter<F> {
late F currentFilter;
Future<void> applyFilter(F filter);
}

Механизм реализует этот интерфейс поверх контроллера пагинации. При применении фильтра сбрасывается индекс и загружается первая страница с новыми параметрами.

Обновление жестом:


Свайп вниз для обновления тоже встраивается. Достаточно добавить метод, который сбрасывает индекс и загружает данные заново. А в интерфейсе обернуть список в RefreshIndicator.

Вывод:


Пагинация на Riverpod может быть переиспользуемой. Контроллер поверх AsyncNotifier хранит индекс, загружает страницы и добавляет новые элементы. Конкретный экран определяет только начальный индекс, запрос и следующий индекс.

Такой подход дает чистую архитектуру, поддержку нечисловых индексов и легкое расширение поиском, фильтрацией и обновлением жестом.
20.10.2026 11 668