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

469 lines
13 KiB
Dart

part of 'widgets.dart';
/// [TypeDefs]
typedef EnhancedListViewKey<B, H, F, Q>
= GlobalKey<EnhancedListViewState<B, H, F, Q>>;
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, Q> = Future<List<T?>> Function(
int page, int pageSize, Q 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, Q>
extends StatefulWidget {
const EnhancedListViewBase({super.key});
}
abstract interface class EnhancedListViewBaseState<T>
extends State<EnhancedListViewBase> {}
class EnhancedListView<BodyType, HeaderType, FooterType, QueryType>
extends EnhancedListViewBase<BodyType, HeaderType, FooterType, QueryType> {
final BodyItemsBuilder<BodyType, QueryType?> bodyItems;
final PaginatedListViewBodyBuilder<BodyType> bodyBuilder;
final HeaderItemsBuilder<HeaderType>? headerItems;
final PaginatedListViewHeaderBuilder<HeaderType>? headerBuilder;
final FooterItemsBuilder<FooterType>? footerItems;
final PaginatedListViewFooterBuilder<FooterType>? footerBuilder;
EnhancedListView({
Key? key,
required this.bodyItems,
required this.bodyBuilder,
this.headerItems,
this.headerBuilder,
this.footerItems,
this.footerBuilder,
}) : super(key: key);
@override
EnhancedListViewState<BodyType, HeaderType, FooterType, QueryType>
createState() =>
EnhancedListViewState<BodyType, HeaderType, FooterType, QueryType>();
}
class EnhancedListViewState<ItemType, HeaderType, FooterType, QueryType>
extends State<
EnhancedListView<ItemType, HeaderType, FooterType, QueryType>> {
late EnhancedListViewBloc<ItemType, HeaderType, FooterType, QueryType> bloc;
@override
void initState() {
super.initState();
bloc = EnhancedListViewBloc<ItemType, HeaderType, FooterType, QueryType>(
bodyItemsBuilder: widget.bodyItems,
headerItemsBuilder: widget.headerItems ?? () async => [],
footerItemsBuilder: widget.footerItems ?? () async => [],
);
bloc.events.loadBodyItems();
bloc.events.loadHeaderItems();
bloc.events.loadFooterItems();
}
void filterBodyItems(Query query) => bloc.filterBodyItems(query);
void filterHeaderItems(Query query) => bloc.filterHeaderItems(query);
void filterFooterItems(Query query) => bloc.filterFooterItems(query);
@override
Widget build(BuildContext context) {
final header = StreamBuilder<List<HeaderType>>(
stream: bloc.states.headerItems.cast<List<HeaderType>>(),
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<List<FooterType>>(
stream: bloc.states.footerItems.cast<List<FooterType>>(),
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<List<ItemType>>(
stream: bloc.states.bodyItems.cast<List<ItemType>>(),
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<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 Management]
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 EnhancedListViewEvents<T, H, F, Q> {
void loadBodyItems({bool reset = false, dynamic query = null});
void loadHeaderItems();
void loadFooterItems();
}
abstract class EnhancedListViewStates<T, H, F, Q> {
Stream<List<T>> get bodyItems;
Stream<List<H>> get headerItems;
Stream<List<F>> get footerItems;
Stream<bool> get isLoading;
Stream<String> get errors;
}
@RxBloc()
class EnhancedListViewBloc<T, H, F, Q>
extends $EnhancedListViewBloc<T, H, F, Q?> {
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<T, Q?> bodyItemsBuilder;
final HeaderItemsBuilder<H> headerItemsBuilder;
final FooterItemsBuilder<F> footerItemsBuilder;
final _bodyItems = BehaviorSubject<List<T>>.seeded([]);
final _headerItems = BehaviorSubject<List<H>>.seeded([]);
final _footerItems = BehaviorSubject<List<F>>.seeded([]);
final _isLoading = BehaviorSubject<bool>.seeded(false);
final _errors = BehaviorSubject<String>();
Stream<List<T>> _fetchBodyItems(bool reset, Q? query) async* {
try {
_isLoading.add(true);
final items = await bodyItemsBuilder(1, 10, query);
yield items.whereType<T>().toList();
} catch (e) {
_errors.add(e.toString());
} finally {
_isLoading.add(false);
}
}
Stream<List<H>> _fetchHeaderItems() async* {
try {
_isLoading.add(true);
final items = await headerItemsBuilder();
yield items.whereType<H>().toList();
} catch (e) {
_errors.add(e.toString());
} finally {
_isLoading.add(false);
}
}
Stream<List<F>> _fetchFooterItems() async* {
try {
_isLoading.add(true);
final items = await footerItemsBuilder();
yield items.whereType<F>().toList();
} catch (e) {
_errors.add(e.toString());
} finally {
_isLoading.add(false);
}
}
void filterBodyItems(Q query) {
_$loadBodyItemsEvent.add((query: query, reset: true));
}
void filterHeaderItems(Q query) {
_$loadHeaderItemsEvent.add(null);
}
void filterFooterItems(Q query) {
_$loadFooterItemsEvent.add(null);
}
@override
void dispose() {
_bodyItems.close();
_headerItems.close();
_footerItems.close();
_isLoading.close();
_errors.close();
super.dispose();
}
@override
Stream<List<T>> _mapToBodyItemsState() => _bodyItems.stream;
@override
Stream<String> _mapToErrorsState() => _errors.stream;
@override
Stream<List<F>> _mapToFooterItemsState() => _footerItems.stream;
@override
Stream<List<H>> _mapToHeaderItemsState() => _headerItems.stream;
@override
Stream<bool> _mapToIsLoadingState() => _isLoading.stream;
}