import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class PaginationState { final List items; final bool isLoading; final bool isReloading; final int? totalCount; final bool hasMore; final String? cursor; const PaginationState({ required this.items, required this.isLoading, required this.isReloading, required this.totalCount, required this.hasMore, required this.cursor, }); PaginationState copyWith({ List? items, bool? isLoading, bool? isReloading, int? totalCount, bool? hasMore, String? cursor, }) { return PaginationState( items: items ?? this.items, isLoading: isLoading ?? this.isLoading, isReloading: isReloading ?? this.isReloading, totalCount: totalCount ?? this.totalCount, hasMore: hasMore ?? this.hasMore, cursor: cursor ?? this.cursor, ); } } abstract class PaginationController { int? get totalCount; int get fetchedCount; bool get fetchedAll; bool get isLoading; bool get isReloading; bool get hasMore; set hasMore(bool value); String? get cursor; set cursor(String? value); FutureOr> fetch(); Future refresh(); Future fetchFurther(); } abstract class PaginationFiltered { late F currentFilter; Future applyFilter(F filter); } mixin AsyncPaginationController on AsyncNotifier> implements PaginationController { @override int? totalCount; @override int get fetchedCount => state.value?.isReloading == true ? 0 : state.value?.items.length ?? 0; @override bool get fetchedAll => !(state.value?.hasMore ?? true) || ((state.value?.totalCount != null && fetchedCount >= state.value!.totalCount!)); @override bool get isLoading => state.value?.isLoading ?? false; @override bool get isReloading => state.value?.isReloading ?? false; @override bool get hasMore => state.value?.hasMore ?? true; @override String? get cursor => state.value?.cursor; @override set hasMore(bool value) { if (state is AsyncData) { state = AsyncData((state as AsyncData).value.copyWith(hasMore: value)); } } @override set cursor(String? value) { if (state is AsyncData) { state = AsyncData((state as AsyncData).value.copyWith(cursor: value)); } } @override FutureOr> build() async { final items = await fetch(); return PaginationState( items: items, isLoading: false, isReloading: false, totalCount: totalCount, hasMore: hasMore, cursor: cursor, ); } @override Future refresh() async { state = AsyncData( state.value!.copyWith( isLoading: true, isReloading: true, totalCount: null, hasMore: true, cursor: null, ), ); final newItems = await fetch(); state = AsyncData( state.value!.copyWith( items: newItems, isLoading: false, isReloading: false, totalCount: totalCount, hasMore: hasMore, cursor: cursor, ), ); } @override Future fetchFurther() async { if (fetchedAll) return; if (isLoading) return; state = AsyncData(state.value!.copyWith(isLoading: true)); final newItems = await fetch(); state = AsyncData( state.value!.copyWith( items: [...state.value!.items, ...newItems], isLoading: false, ), ); } } mixin AsyncPaginationFilter on AsyncPaginationController implements PaginationFiltered { @override Future applyFilter(F filter) async { if (currentFilter == filter) return; state = AsyncData( state.value!.copyWith( isReloading: true, isLoading: true, totalCount: null, hasMore: true, cursor: null, ), ); currentFilter = filter; final newItems = await fetch(); state = AsyncData( state.value!.copyWith( items: newItems, isLoading: false, isReloading: false, totalCount: totalCount, hasMore: hasMore, cursor: cursor, ), ); } }