part of 'widgets.dart'; /// [TypeDefs] ---------------------------------------------------- typedef EnhancedListViewKey = GlobalKey< EnhancedListViewState>; typedef PaginatedListViewHeaderBuilder = Widget Function( Future> Function() headerItems); typedef PaginatedListViewBodyBuilder = Widget Function( BuildContext context, BodyType item, int index); typedef PaginatedListViewFooterBuilder = Widget Function( Future> Function() footerItems); typedef Query = QueryType?; typedef BodyItemsBuilder = Future> Function(int page, int pageSize, QueryType 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(); }); } /// [Interfaces] ---------------------------------------------------- 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< EnhancedListViewBase> {} /// [Mixins] ---------------------------------------------------- mixin EnhancedListViewMixin { late EnhancedListViewBloc bloc; void filterBodyItems(QueryType query) => bloc.filterBodyItems(query); void filterHeaderItems(QueryType query) => bloc.filterHeaderItems(query); void filterFooterItems(QueryType query) => bloc.filterFooterItems(query); } /// [Widgets] ---------------------------------------------------- class EnhancedListView extends EnhancedListViewBase { final BodyItemsBuilder bodyItems; final PaginatedListViewBodyBuilder bodyBuilder; final HeaderItemsBuilder? headerItems; final PaginatedListViewHeaderBuilder? headerBuilder; final FooterItemsBuilder? footerItems; final PaginatedListViewFooterBuilder? footerBuilder; const 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> with EnhancedListViewMixin { @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) { final header = StreamBuilder>( stream: bloc.states.headerItems.cast>(), builder: (context, headerSnapshot) { if (headerSnapshot.connectionState == ConnectionState.waiting) { return const EnhancedProgressIndicator(); } else if (headerSnapshot.hasError) { return EnhancedErrorWidget(error: headerSnapshot.error); } else if (!headerSnapshot.hasData || headerSnapshot.data!.isEmpty) { return const SizedBox.shrink(); } else { return widget.headerBuilder!(() async => headerSnapshot.data!); } }, ); final footer = StreamBuilder>( stream: bloc.states.footerItems.cast>(), builder: (context, footerSnapshot) { if (footerSnapshot.connectionState == ConnectionState.waiting) { return const EnhancedProgressIndicator(); } else if (footerSnapshot.hasError) { return EnhancedErrorWidget(error: footerSnapshot.error); } else if (!footerSnapshot.hasData || footerSnapshot.data!.isEmpty) { return const SizedBox.shrink(); } else { return widget.footerBuilder!(() async => footerSnapshot.data!); } }, ); final body = Expanded( child: StreamBuilder>( stream: bloc.states.bodyItems.cast>(), builder: (context, bodySnapshot) { if (bodySnapshot.connectionState == ConnectionState.waiting) { return const EnhancedProgressIndicator(); } else if (bodySnapshot.hasError) { return EnhancedErrorWidget(error: bodySnapshot.error); } else if (!bodySnapshot.hasData || bodySnapshot.data!.isEmpty) { return const SizedBox.shrink(); } else { return ListView.builder( itemCount: bodySnapshot.data?.length ?? 0, itemBuilder: (context, index) { return widget.bodyBuilder( context, bodySnapshot.data![index], index); }, ); } }, ), ); return Column( children: [ if (widget.headerBuilder != null) header, body, if (widget.footerBuilder != null) footer, ], ); } @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); } /// [Blocs] ---------------------------------------------------- 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, dynamic query = null}); void loadHeaderItems(); void loadFooterItems(); } abstract class EnhancedListViewStates { Stream> get bodyItems; Stream> get headerItems; Stream> get footerItems; Stream get isLoading; Stream get errors; } class EnhancedListViewBloc extends $EnhancedListViewBloc { EnhancedListViewBloc({ required this.bodyItemsBuilder, required this.headerItemsBuilder, required this.footerItemsBuilder, }) { _$loadBodyItemsEvent .startWith((query: null, reset: true)) .switchMap((item) => _fetchBodyItems(item.reset, item.query)) .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, QueryType? query) async* { try { _isLoading.add(true); final items = await bodyItemsBuilder(1, 10, query); 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); } } void filterBodyItems(QueryType query) { _$loadBodyItemsEvent.add((query: query, reset: true)); } void filterHeaderItems(QueryType query) { _$loadHeaderItemsEvent.add(null); } void filterFooterItems(QueryType query) { _$loadFooterItemsEvent.add(null); } @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; } abstract class EnhancedListViewBlocType extends RxBlocTypeBase { EnhancedListViewEvents get events; EnhancedListViewStates get states; } abstract class $EnhancedListViewBloc extends RxBlocBase implements EnhancedListViewEvents, EnhancedListViewStates, EnhancedListViewBlocType { final _compositeSubscription = CompositeSubscription(); /// Тhe [Subject] where events sink to by calling [loadBodyItems] final _$loadBodyItemsEvent = PublishSubject<({bool reset, dynamic query})>(); /// Тhe [Subject] where events sink to by calling [loadHeaderItems] final _$loadHeaderItemsEvent = PublishSubject(); /// Тhe [Subject] where events sink to by calling [loadFooterItems] final _$loadFooterItemsEvent = PublishSubject(); /// The state of [bodyItems] implemented in [_mapToBodyItemsState] late final Stream> _bodyItemsState = _mapToBodyItemsState(); /// The state of [headerItems] implemented in [_mapToHeaderItemsState] late final Stream> _headerItemsState = _mapToHeaderItemsState(); /// The state of [footerItems] implemented in [_mapToFooterItemsState] late final Stream> _footerItemsState = _mapToFooterItemsState(); /// The state of [isLoading] implemented in [_mapToIsLoadingState] late final Stream _isLoadingState = _mapToIsLoadingState(); /// The state of [errors] implemented in [_mapToErrorsState] late final Stream _errorsState = _mapToErrorsState(); @override void loadBodyItems({ bool reset = false, dynamic query = null, }) => _$loadBodyItemsEvent.add(( reset: reset, query: query, )); @override void loadHeaderItems() => _$loadHeaderItemsEvent.add(null); @override void loadFooterItems() => _$loadFooterItemsEvent.add(null); @override Stream> get bodyItems => _bodyItemsState; @override Stream> get headerItems => _headerItemsState; @override Stream> get footerItems => _footerItemsState; @override Stream get isLoading => _isLoadingState; @override Stream get errors => _errorsState; Stream> _mapToBodyItemsState(); Stream> _mapToHeaderItemsState(); Stream> _mapToFooterItemsState(); Stream _mapToIsLoadingState(); Stream _mapToErrorsState(); @override EnhancedListViewEvents get events => this; @override EnhancedListViewStates get states => this; @override void dispose() { _$loadBodyItemsEvent.close(); _$loadHeaderItemsEvent.close(); _$loadFooterItemsEvent.close(); _compositeSubscription.dispose(); super.dispose(); } } // ignore: unused_element typedef _LoadBodyItemsEventArgs = ({bool reset, dynamic query});