part of 'widgets.dart'; /// [TypeDefs] typedef EnhancedListViewKey = GlobalKey>; typedef PaginatedListViewHeaderBuilder = Widget Function( Future> Function() headerItems); typedef PaginatedListViewBodyBuilder = Widget Function( BuildContext context, T item, int index); typedef PaginatedListViewFooterBuilder = Widget Function( Future> Function() footerItems); typedef Query = T?; typedef BodyItemsBuilder = Future> Function( int page, int pageSize, Query query); typedef HeaderItemsBuilder = Future> Function(); typedef FooterItemsBuilder = Future> Function(); /// [Extensions] extension PaginatedListMergeExtensions on Stream>> { Stream> mergeWithPaginatedList( BehaviorSubject> currentList) { return map( (result) { final current = currentList.value; if (result is ResultSuccess>) { final newPaginated = result.data; return current.items.isEmpty ? newPaginated : current.copyWith( list: [...current.items, ...newPaginated.items], currentPage: newPaginated.currentPage, totalCount: newPaginated.totalCount, error: newPaginated.error, ); } else if (result is ResultError>) { return current.copyWith(error: result.error); } else { return current; } }, ); } } extension PublishSubjectExtensions on PublishSubject { Stream startWith(T initial) => Rx.concat([Stream.value(initial), this]); } extension StreamStartWithExtension on Stream { Stream startWith(T initial) => Rx.concat([Stream.value(initial), this]); } extension QueryBlocStreamExtensions on Stream { Stream>> fetchData( EnhancedListViewRepository repository, PaginatedListViewBodyBuilder builder, BehaviorSubject> paginatedList, ) => switchMap((reset) { if (reset) { paginatedList.add(paginatedList.value.resetAll()); } final nextPage = paginatedList.value.currentPage + 1; return repository .fetchPage(nextPage, paginatedList.value.pageSize, builder) .asResultStream(); }); } /// [Widgets] /// [EnhancedListView] interface class EnhancedPaginatedList extends PaginatedList { @override final Exception? error; final bool isInitialized; @override final bool isLoading; final List items; @override final int pageSize; @override final int? totalCount; final int currentPage; EnhancedPaginatedList({ required this.items, required this.pageSize, this.currentPage = 0, this.totalCount, required this.error, required this.isInitialized, required this.isLoading, }) : super( error: error, isInitialized: isInitialized, isLoading: isLoading, list: items, pageSize: pageSize, totalCount: totalCount, ); EnhancedPaginatedList resetAll() => EnhancedPaginatedList( error: null, isInitialized: false, isLoading: false, items: const [], pageSize: pageSize, totalCount: null, currentPage: 0, ); @override EnhancedPaginatedList copyWith({ List? list, bool? isLoading, int? totalCount, Exception? error, int? pageSize, bool? isInitialized, int? currentPage, }) => EnhancedPaginatedList( error: error ?? this.error, isInitialized: isInitialized ?? this.isInitialized, isLoading: isLoading ?? this.isLoading, items: list ?? this.items, pageSize: pageSize ?? this.pageSize, totalCount: totalCount ?? this.totalCount, currentPage: currentPage ?? this.currentPage, ); @override int get itemCount => items.length; @override T? getItem(int index) => index < items.length ? items[index] : null; Future awaitLoad() async => Future.value(); } abstract interface class EnhancedListViewBase extends StatefulWidget { const EnhancedListViewBase({super.key}); } abstract interface class EnhancedListViewBaseState extends State {} // ignore: must_be_immutable class EnhancedListView extends EnhancedListViewBase { BodyItemsBuilder bodyItems; PaginatedListViewBodyBuilder bodyBuilder; HeaderItemsBuilder? headerItems; PaginatedListViewHeaderBuilder? headerBuilder; FooterItemsBuilder? footerItems; PaginatedListViewFooterBuilder? footerBuilder; EnhancedListView({ Key? key, required this.bodyItems, required this.bodyBuilder, this.headerItems, this.headerBuilder, this.footerItems, this.footerBuilder, }) : super(key: key); @override EnhancedListViewState createState() => EnhancedListViewState(); } class EnhancedListViewState extends State> { final ScrollController _scrollController = ScrollController(); bool _isLoadingMore = false; List items = []; List headerItems = []; List footerItems = []; int currentPage = 1; Query query; @override void initState() { super.initState(); _scrollController.addListener(_onScroll); _loadBodyItems(); _loadHeaderItems(); _loadFooterItems(); } void _onScroll() { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent && !_isLoadingMore) { _loadMoreBodyItems(); } } Future _loadBodyItems() async { log('loadInitialItems'); final newItems = await widget.bodyItems(1, 10, query); setState(() { items = newItems; }); } Future _loadMoreBodyItems() async { log('loadMoreItems'); setState(() { _isLoadingMore = true; }); final newItems = await widget.bodyItems(currentPage + 1, 10, query); setState(() { _isLoadingMore = false; if (newItems.isNotEmpty) { items.addAll(newItems); currentPage++; } }); } Future _loadHeaderItems() async { if (widget.headerItems != null) { log('loadHeaderItems'); final newHeaderItems = await widget.headerItems!(); setState(() { headerItems = newHeaderItems; }); } } Future _loadFooterItems() async { if (widget.footerItems != null) { log('loadFooterItems'); final newFooterItems = await widget.footerItems!(); setState(() { footerItems = newFooterItems; }); } } Future filterBodyItems(Query newQuery) async { log('filterItems B: ${newQuery.toString()}'); setState(() { query = newQuery; items = []; currentPage = 1; }); await _loadBodyItems(); } @override Widget build(BuildContext context) { log('key: ${widget.key}'); return ListView.builder( controller: _scrollController, itemCount: items.length + (headerItems.isNotEmpty ? 1 : 0) + (footerItems.isNotEmpty ? 1 : 0) + (_isLoadingMore ? 1 : 0), itemBuilder: (context, index) { if (headerItems.isNotEmpty && index == 0) { return widget.headerBuilder!(() => Future.value(headerItems)); } if (footerItems.isNotEmpty && index == items.length + (headerItems.isNotEmpty ? 1 : 0)) { return widget.footerBuilder!(() => Future.value(footerItems)); } if (_isLoadingMore && index == items.length + (headerItems.isNotEmpty ? 1 : 0) + (footerItems.isNotEmpty ? 1 : 0)) { return const EnhancedProgressIndicator(); } final item = items[index - (headerItems.isNotEmpty ? 1 : 0)]; return widget.bodyBuilder(context, item as ItemType, index); }, ); } @override void dispose() { _scrollController.dispose(); super.dispose(); } } /// [Utils] class EnhancedListTile extends StatelessWidget { const EnhancedListTile( {required this.leading, required this.title, super.key}); final T leading; final T title; @override Widget build(BuildContext context) { return Card( child: ListTile( leading: leading, title: title, ), ); } } class EnhancedErrorWidget extends StatelessWidget { final Object? error; const EnhancedErrorWidget({required this.error, super.key}); @override Widget build(BuildContext context) { log('error: $error'); return Padding( padding: const EdgeInsets.all(16.0), child: Text( error.toString(), style: const TextStyle(color: Colors.red), ), ); } } class EnhancedProgressIndicator extends StatelessWidget { const EnhancedProgressIndicator({super.key}); @override Widget build(BuildContext context) => const Center( child: Padding( padding: EdgeInsets.symmetric(vertical: 12), child: CircularProgressIndicator(), ), ); } class NetworkError implements Exception { final String message; NetworkError(this.message); } class ParsingError implements Exception { final String message; ParsingError(this.message); } class AuthorizationError implements Exception { final String message; AuthorizationError(this.message); } /// [State Managment] Stream get loadingState => Stream.empty(); Stream get errorState => Stream.empty(); abstract class EnhancedListViewRepository { Future> fetchPage( int page, int pageSize, PaginatedListViewBodyBuilder builder); } abstract class EnhancedListViewBlocStates { Stream get isLoading; Stream get errors; Stream> get paginatedList; @RxBlocIgnoreState() Future get refreshDone; } abstract class EnhancedListViewEvents { void loadPage({bool reset = false}); } abstract class EnhancedListViewBlocType extends RxBlocTypeBase { EnhancedListViewEvents get events; EnhancedListViewBlocStates get states; } abstract class $EnhancedListViewBloc extends RxBlocBase implements EnhancedListViewEvents, EnhancedListViewBlocStates, EnhancedListViewBlocType { final _compositeSubscription = CompositeSubscription(); final _$loadPageEvent = PublishSubject(); late final Stream _isLoadingState = _mapToIsLoadingState(); late final Stream _errorsState = _mapToErrorsState(); late final Stream> _paginatedListState = _mapToPaginatedListState(); @override void loadPage({bool reset = false}) => _$loadPageEvent.add(reset); @override Stream get isLoading => _isLoadingState; @override Stream get errors => _errorsState; @override Stream> get paginatedList => _paginatedListState; Stream _mapToIsLoadingState(); Stream _mapToErrorsState(); Stream> _mapToPaginatedListState(); @override EnhancedListViewEvents get events => this; @override EnhancedListViewBlocStates get states => this; @override void dispose() { _$loadPageEvent.close(); _compositeSubscription.dispose(); super.dispose(); } } class EnhancedListViewBloc extends $EnhancedListViewBloc { EnhancedListViewBloc({ required EnhancedListViewRepository repository, required PaginatedListViewBodyBuilder builder, required T item, int initialPageSize = 50, }) { _$loadPageEvent .startWith(true) .fetchData( repository, (context, item, index) => builder(context, item, index), _paginatedList, ) .setResultStateHandler(this) .mergeWithPaginatedList(_paginatedList) .bind(_paginatedList) .addTo(_compositeSubscription); } final _paginatedList = BehaviorSubject>.seeded( EnhancedPaginatedList( items: [], pageSize: 1, currentPage: 1, error: Exception(), isInitialized: true, isLoading: false, totalCount: 0, ), ); @override Future get refreshDone async => _paginatedList.value.awaitLoad(); @override Stream> _mapToPaginatedListState() => _paginatedList; @override Stream _mapToErrorsState() => errorState.map((error) => error.toString()); @override Stream _mapToIsLoadingState() => loadingState; @override void dispose() { _paginatedList.close(); super.dispose(); } }