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 {} class EnhancedListView extends EnhancedListViewBase { final BodyItemsBuilder bodyItems; final PaginatedListViewBodyBuilder bodyBuilder; final HeaderItemsBuilder? headerItems; final PaginatedListViewHeaderBuilder? headerBuilder; final FooterItemsBuilder? footerItems; final 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> { late EnhancedListViewBloc bloc; @override void initState() { super.initState(); bloc = EnhancedListViewBloc( bodyItemsBuilder: widget.bodyItems, headerItemsBuilder: widget.headerItems ?? () async => [], footerItemsBuilder: widget.footerItems ?? () async => [], ); bloc.events.loadBodyItems(); bloc.events.loadHeaderItems(); bloc.events.loadFooterItems(); } @override Widget build(BuildContext context) { return StreamBuilder>( stream: bloc.states.bodyItems.cast>(), builder: (context, bodySnapshot) { return StreamBuilder>( stream: bloc.states.headerItems.cast>(), builder: (context, headerSnapshot) { return StreamBuilder>( stream: bloc.states.footerItems.cast>(), builder: (context, footerSnapshot) { return ListView.builder( itemCount: bodySnapshot.data?.length ?? 0, itemBuilder: (context, index) { return widget.bodyBuilder( context, bodySnapshot.data![index], index); }, ); }, ); }, ); }, ); } @override void dispose() { bloc.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 Management] Stream get loadingState => Stream.empty(); Stream get errorState => Stream.empty(); abstract class EnhancedListViewRepository { Future> fetchPage( int page, int pageSize, PaginatedListViewBodyBuilder builder); } abstract class EnhancedListViewEvents { void loadBodyItems({bool reset = false}); void loadHeaderItems(); void loadFooterItems(); } abstract class EnhancedListViewStates { Stream> get bodyItems; Stream> get headerItems; Stream> get footerItems; Stream get isLoading; Stream get errors; } @RxBloc() class EnhancedListViewBloc extends $EnhancedListViewBloc { EnhancedListViewBloc({ required this.bodyItemsBuilder, required this.headerItemsBuilder, required this.footerItemsBuilder, }) { _$loadBodyItemsEvent .startWith(true) .switchMap((reset) => _fetchBodyItems(reset)) .bind(_bodyItems) .addTo(_compositeSubscription); _$loadHeaderItemsEvent .switchMap((_) => _fetchHeaderItems()) .bind(_headerItems) .addTo(_compositeSubscription); _$loadFooterItemsEvent .switchMap((_) => _fetchFooterItems()) .bind(_footerItems) .addTo(_compositeSubscription); } final BodyItemsBuilder bodyItemsBuilder; final HeaderItemsBuilder headerItemsBuilder; final FooterItemsBuilder footerItemsBuilder; final _bodyItems = BehaviorSubject>.seeded([]); final _headerItems = BehaviorSubject>.seeded([]); final _footerItems = BehaviorSubject>.seeded([]); final _isLoading = BehaviorSubject.seeded(false); final _errors = BehaviorSubject(); Stream> _fetchBodyItems(bool reset) async* { try { _isLoading.add(true); final items = await bodyItemsBuilder(1, 10, null); yield items.whereType().toList(); } catch (e) { _errors.add(e.toString()); } finally { _isLoading.add(false); } } Stream> _fetchHeaderItems() async* { try { _isLoading.add(true); final items = await headerItemsBuilder(); yield items.whereType().toList(); } catch (e) { _errors.add(e.toString()); } finally { _isLoading.add(false); } } Stream> _fetchFooterItems() async* { try { _isLoading.add(true); final items = await footerItemsBuilder(); yield items.whereType().toList(); } catch (e) { _errors.add(e.toString()); } finally { _isLoading.add(false); } } @override void dispose() { _bodyItems.close(); _headerItems.close(); _footerItems.close(); _isLoading.close(); _errors.close(); super.dispose(); } @override Stream> _mapToBodyItemsState() => _bodyItems.stream; @override Stream _mapToErrorsState() => _errors.stream; @override Stream> _mapToFooterItemsState() => _footerItems.stream; @override Stream> _mapToHeaderItemsState() => _headerItems.stream; @override Stream _mapToIsLoadingState() => _isLoading.stream; }