part of 'widgets.dart'; /// [TypeDefs] typedef EnhancedRemoteListViewKey = GlobalKey>; typedef EnhancedLocalListViewKey = 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 { static EnhancedRemoteListView remote({ required Key? key, required BodyItemsBuilder bodyItems, required PaginatedListViewBodyBuilder bodyBuilder, HeaderItemsBuilder? headerItems, PaginatedListViewHeaderBuilder? headerBuilder, FooterItemsBuilder? footerItems, PaginatedListViewFooterBuilder? footerBuilder, }) { return EnhancedRemoteListView( key: key, bodyItems: bodyItems, bodyBuilder: bodyBuilder, headerItems: headerItems, headerBuilder: headerBuilder, footerItems: footerItems, footerBuilder: footerBuilder, ); } static EnhancedLocalListView local({ required Key? key, required List list, required Widget Function(T) itemBuilder, required bool Function(T, String) filter, List Function(String)? onSearch, Widget? header, }) { return EnhancedLocalListView( key: key, list: list, itemBuilder: itemBuilder, filter: filter, onSearch: onSearch, header: header, ); } } /// [EnhancedLocalListView] class EnhancedLocalListView extends EnhancedListViewBase { final List list; final Widget Function(T) itemBuilder; final bool Function(T, String) filter; final Widget header; final List Function(String)? onSearch; EnhancedLocalListView({ Key? key, required this.list, required this.itemBuilder, required this.filter, List Function(String)? onSearch, Widget? header, }) : header = header ?? const SizedBox.shrink(), onSearch = onSearch ?? ((String query) => list.where((documents) => filter(documents, query)).toList()), super(key: key); @override EnhancedLocalListViewState createState() => EnhancedLocalListViewState(); } class EnhancedLocalListViewState extends State> { TextEditingController editingController = TextEditingController(); late List filteredItems; @override void initState() { filteredItems = widget.list; super.initState(); } @override Widget build(BuildContext context) { void filter(value) { setState(() { filteredItems = widget.onSearch!(value); }); } return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.max, children: [ Expanded( child: ListView.builder( shrinkWrap: true, itemCount: filteredItems.length + 1, itemBuilder: (context, index) { if (index == 0) return widget.header; return widget.itemBuilder(filteredItems[index - 1]); }, ), ), Padding( padding: const EdgeInsets.all(30.0), child: TextFormField( controller: editingController, onChanged: filter, cursorColor: Colors.black, cursorWidth: 2.0, cursorRadius: Radius.circular(2.0), style: TextStyle( color: Colors.black, fontSize: 16.0, ), keyboardType: TextInputType.text, textInputAction: TextInputAction.search, autocorrect: true, textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( prefixIcon: Icon(Icons.search, color: Colors.black), labelText: 'Pesquisar', labelStyle: TextStyle( color: Colors.black, fontSize: 16.0, ), hintText: 'Digite sua pesquisa', hintStyle: TextStyle( color: Colors.grey, fontSize: 14.0, ), filled: true, fillColor: Colors.white, contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(15.0)), borderSide: BorderSide(color: Colors.black), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(15.0)), borderSide: BorderSide(color: Colors.blue), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(15.0)), borderSide: BorderSide(color: Colors.red), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(15.0)), borderSide: BorderSide(color: Colors.red, width: 2.0), ), ), ), ), ], ); } } /// [EnhancedRemoteListView] class EnhancedRemoteListView extends EnhancedListViewBase { final BodyItemsBuilder bodyItems; final PaginatedListViewBodyBuilder bodyBuilder; final HeaderItemsBuilder? headerItems; final PaginatedListViewHeaderBuilder? headerBuilder; final FooterItemsBuilder? footerItems; final PaginatedListViewFooterBuilder? footerBuilder; const EnhancedRemoteListView({ Key? key, required this.bodyItems, required this.bodyBuilder, this.headerItems, this.headerBuilder, this.footerItems, this.footerBuilder, }) : super(key: key); @override EnhancedRemoteListViewState createState() => EnhancedRemoteListViewState(); } class EnhancedRemoteListViewState extends State> { final ScrollController _scrollController = ScrollController(); bool _isLoadingMore = false; List _items = []; int _currentPage = 1; Query query; @override void initState() { super.initState(); _scrollController.addListener(_onScroll); _loadInitialItems(); } void _onScroll() { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent && !_isLoadingMore) { _loadMoreItems(); } } Future _loadInitialItems() async { final newItems = await widget.bodyItems(1, 10, query); setState(() { _items = newItems; }); } Future _loadMoreItems() async { setState(() { _isLoadingMore = true; }); final newItems = await widget.bodyItems(_currentPage + 1, 10, query); setState(() { _isLoadingMore = false; if (newItems.isNotEmpty) { _items.addAll(newItems); _currentPage++; } }); } Future filterItems(Query newQuery) async { log('filterItems: $newQuery'); setState(() { query = newQuery; _items = []; _currentPage = 1; }); await _loadInitialItems(); } @override Widget build(BuildContext context) { log('key: ${widget.key}'); return ListView.builder( controller: _scrollController, itemCount: _items.length + (widget.headerItems != null ? 1 : 0) + (widget.footerItems != null ? 1 : 0) + (_isLoadingMore ? 1 : 0), itemBuilder: (context, index) { if (widget.headerItems != null && index == 0) { return FutureBuilder>( future: widget.headerItems!(), builder: (context, headerSnapshot) { if (headerSnapshot.connectionState == ConnectionState.waiting) { return const EnhancedProgressIndicator(); } else if (headerSnapshot.hasError) { return EnhancedErrorWidget(error: headerSnapshot.error); } else { return widget .headerBuilder!(() => Future.value(headerSnapshot.data)); } }, ); } if (widget.footerItems != null && index == _items.length + (widget.headerItems != null ? 1 : 0)) { return FutureBuilder>( future: widget.footerItems!(), builder: (context, footerSnapshot) { if (footerSnapshot.connectionState == ConnectionState.waiting) { return const EnhancedProgressIndicator(); } else if (footerSnapshot.hasError) { return EnhancedErrorWidget( error: footerSnapshot.error as Exception); } else { return widget .footerBuilder!(() => Future.value(footerSnapshot.data)); } }, ); } if (_isLoadingMore && index == _items.length + (widget.headerItems != null ? 1 : 0) + (widget.footerItems != null ? 1 : 0)) { return const EnhancedProgressIndicator(); } final item = _items[index - (widget.headerItems != null ? 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(); } }