
Бесконечная лента - привычная часть многих приложений. Но реализация пагинации часто превращается в повторяющийся код. Хочется один раз собрать универсальный механизм и переиспользовать его в разных экранах. В статье на примере 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(...),
)
Когда пользователь прокручивает список до конца, вызывается загрузка следующей страницы.
🔗 Читать подробнее
💡 Вывод:
Пагинация на Riverpod может быть переиспользуемой. Контроллер поверх AsyncNotifier хранит индекс, загружает страницы и добавляет новые элементы. Конкретный экран определяет только начальный индекс, запрос и следующий индекс.
Такой подход дает чистую архитектуру, поддержку нечисловых индексов и легкое расширение поиском, фильтрацией и обновлением жестом.
Подписаться на канал:
➡️ Flutter & Dart | Мобильный трудоголик









