flutter-freaccess-hub/lib/shared/widgets/enhanced_list_view.dart

638 lines
18 KiB
Dart

part of 'widgets.dart';
/// [TypeDefs]
typedef EnhancedRemoteListViewKey<B, H, F>
= GlobalKey<EnhancedRemoteListViewState<B, H, F>>;
typedef EnhancedLocalListViewKey<B, H, F>
= GlobalKey<EnhancedLocalListViewState<B, H, F>>;
typedef PaginatedListViewHeaderBuilder<H> = Widget Function(
Future<List<H?>> Function() headerItems);
typedef PaginatedListViewBodyBuilder<T> = Widget Function(
BuildContext context, T item, int index);
typedef PaginatedListViewFooterBuilder<F> = Widget Function(
Future<List<F?>> Function() footerItems);
typedef Query<T> = T?;
typedef BodyItemsBuilder<T> = Future<List<T?>> Function(
int page, int pageSize, Query query);
typedef HeaderItemsBuilder<H> = Future<List<H?>> Function();
typedef FooterItemsBuilder<F> = Future<List<F?>> Function();
/// [Extensions]
extension PaginatedListMergeExtensions<T>
on Stream<Result<EnhancedPaginatedList<T>>> {
Stream<EnhancedPaginatedList<T>> mergeWithPaginatedList(
BehaviorSubject<EnhancedPaginatedList<T>> currentList) {
return map(
(result) {
final current = currentList.value;
if (result is ResultSuccess<EnhancedPaginatedList<T>>) {
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<EnhancedPaginatedList<T>>) {
return current.copyWith(error: result.error);
} else {
return current;
}
},
);
}
}
extension PublishSubjectExtensions<T> on PublishSubject<T> {
Stream<T> startWith(T initial) => Rx.concat([Stream.value(initial), this]);
}
extension StreamStartWithExtension<T> on Stream<T> {
Stream<T> startWith(T initial) => Rx.concat([Stream.value(initial), this]);
}
extension QueryBlocStreamExtensions<T> on Stream<bool> {
Stream<Result<EnhancedPaginatedList<T>>> fetchData(
EnhancedListViewRepository<T> repository,
PaginatedListViewBodyBuilder<T> builder,
BehaviorSubject<EnhancedPaginatedList<T>> 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<T> extends PaginatedList<T> {
@override
final Exception? error;
final bool isInitialized;
@override
final bool isLoading;
final List<T> 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<T> resetAll() => EnhancedPaginatedList<T>(
error: null,
isInitialized: false,
isLoading: false,
items: const [],
pageSize: pageSize,
totalCount: null,
currentPage: 0,
);
@override
EnhancedPaginatedList<T> copyWith({
List<T>? list,
bool? isLoading,
int? totalCount,
Exception? error,
int? pageSize,
bool? isInitialized,
int? currentPage,
}) =>
EnhancedPaginatedList<T>(
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<void> awaitLoad() async => Future.value();
}
abstract interface class EnhancedListViewBase<T, H, F> extends StatefulWidget {
const EnhancedListViewBase({super.key});
}
abstract interface class EnhancedListViewBaseState<T>
extends State<EnhancedListViewBase> {}
class EnhancedListView {
static EnhancedRemoteListView<BodyType, HeaderType, FooterType>
remote<BodyType, HeaderType, FooterType>({
required Key? key,
required BodyItemsBuilder<BodyType> bodyItems,
required PaginatedListViewBodyBuilder<BodyType> bodyBuilder,
HeaderItemsBuilder<HeaderType>? headerItems,
PaginatedListViewHeaderBuilder<HeaderType>? headerBuilder,
FooterItemsBuilder<FooterType>? footerItems,
PaginatedListViewFooterBuilder<FooterType>? footerBuilder,
}) {
return EnhancedRemoteListView<BodyType, HeaderType, FooterType>(
key: key,
bodyItems: bodyItems,
bodyBuilder: bodyBuilder,
headerItems: headerItems,
headerBuilder: headerBuilder,
footerItems: footerItems,
footerBuilder: footerBuilder,
);
}
static EnhancedLocalListView<T, H, F> local<T, H, F>({
required Key? key,
required List<T> list,
required Widget Function(T) itemBuilder,
required bool Function(T, String) filter,
List<T> Function(String)? onSearch,
Widget? header,
}) {
return EnhancedLocalListView<T, H, F>(
key: key,
list: list,
itemBuilder: itemBuilder,
filter: filter,
onSearch: onSearch,
header: header,
);
}
}
/// [EnhancedLocalListView]
class EnhancedLocalListView<T, H, F> extends EnhancedListViewBase<T, H, F> {
final List<T> list;
final Widget Function(T) itemBuilder;
final bool Function(T, String) filter;
final Widget header;
final List<T> Function(String)? onSearch;
EnhancedLocalListView({
Key? key,
required this.list,
required this.itemBuilder,
required this.filter,
List<T> 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<T, H, F> createState() =>
EnhancedLocalListViewState<T, H, F>();
}
class EnhancedLocalListViewState<T, H, F>
extends State<EnhancedLocalListView<T, H, F>> {
TextEditingController editingController = TextEditingController();
late List<T> 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: <Widget>[
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<BodyType, HeaderType, FooterType>
extends EnhancedListViewBase<BodyType, HeaderType, FooterType> {
final BodyItemsBuilder<BodyType> bodyItems;
final PaginatedListViewBodyBuilder<BodyType> bodyBuilder;
final HeaderItemsBuilder<HeaderType>? headerItems;
final PaginatedListViewHeaderBuilder<HeaderType>? headerBuilder;
final FooterItemsBuilder<FooterType>? footerItems;
final PaginatedListViewFooterBuilder<FooterType>? footerBuilder;
const EnhancedRemoteListView({
Key? key,
required this.bodyItems,
required this.bodyBuilder,
this.headerItems,
this.headerBuilder,
this.footerItems,
this.footerBuilder,
}) : super(key: key);
@override
EnhancedRemoteListViewState<BodyType, HeaderType, FooterType> createState() =>
EnhancedRemoteListViewState<BodyType, HeaderType, FooterType>();
}
class EnhancedRemoteListViewState<ItemType, HeaderType, FooterType>
extends State<EnhancedRemoteListView<ItemType, HeaderType, FooterType>> {
final ScrollController _scrollController = ScrollController();
bool _isLoadingMore = false;
List<ItemType?> _items = [];
int _currentPage = 1;
Query<ItemType> query;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
_loadInitialItems();
}
void _onScroll() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent &&
!_isLoadingMore) {
_loadMoreItems();
}
}
Future<void> _loadInitialItems() async {
final newItems = await widget.bodyItems(1, 10, query);
setState(() {
_items = newItems;
});
}
Future<void> _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<void> filterItems(Query<ItemType> 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<List<HeaderType?>>(
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<List<FooterType?>>(
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<T extends Widget> 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<bool> get loadingState => Stream<bool>.empty();
Stream<Exception> get errorState => Stream<Exception>.empty();
abstract class EnhancedListViewRepository<T> {
Future<EnhancedPaginatedList<T>> fetchPage(
int page, int pageSize, PaginatedListViewBodyBuilder<T> builder);
}
abstract class EnhancedListViewBlocStates<T> {
Stream<bool> get isLoading;
Stream<String> get errors;
Stream<EnhancedPaginatedList<T>> get paginatedList;
@RxBlocIgnoreState()
Future<void> get refreshDone;
}
abstract class EnhancedListViewEvents<T> {
void loadPage({bool reset = false});
}
abstract class EnhancedListViewBlocType<T> extends RxBlocTypeBase {
EnhancedListViewEvents<T> get events;
EnhancedListViewBlocStates<T> get states;
}
abstract class $EnhancedListViewBloc<T> extends RxBlocBase
implements
EnhancedListViewEvents<T>,
EnhancedListViewBlocStates<T>,
EnhancedListViewBlocType<T> {
final _compositeSubscription = CompositeSubscription();
final _$loadPageEvent = PublishSubject<bool>();
late final Stream<bool> _isLoadingState = _mapToIsLoadingState();
late final Stream<String> _errorsState = _mapToErrorsState();
late final Stream<EnhancedPaginatedList<T>> _paginatedListState =
_mapToPaginatedListState();
@override
void loadPage({bool reset = false}) => _$loadPageEvent.add(reset);
@override
Stream<bool> get isLoading => _isLoadingState;
@override
Stream<String> get errors => _errorsState;
@override
Stream<EnhancedPaginatedList<T>> get paginatedList => _paginatedListState;
Stream<bool> _mapToIsLoadingState();
Stream<String> _mapToErrorsState();
Stream<EnhancedPaginatedList<T>> _mapToPaginatedListState();
@override
EnhancedListViewEvents<T> get events => this;
@override
EnhancedListViewBlocStates<T> get states => this;
@override
void dispose() {
_$loadPageEvent.close();
_compositeSubscription.dispose();
super.dispose();
}
}
class EnhancedListViewBloc<T> extends $EnhancedListViewBloc<T> {
EnhancedListViewBloc({
required EnhancedListViewRepository<T> repository,
required PaginatedListViewBodyBuilder<T> 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<EnhancedPaginatedList<T>>.seeded(
EnhancedPaginatedList<T>(
items: [],
pageSize: 1,
currentPage: 1,
error: Exception(),
isInitialized: true,
isLoading: false,
totalCount: 0,
),
);
@override
Future<void> get refreshDone async => _paginatedList.value.awaitLoad();
@override
Stream<EnhancedPaginatedList<T>> _mapToPaginatedListState() => _paginatedList;
@override
Stream<String> _mapToErrorsState() =>
errorState.map((error) => error.toString());
@override
Stream<bool> _mapToIsLoadingState() => loadingState;
@override
void dispose() {
_paginatedList.close();
super.dispose();
}
}