469 lines
13 KiB
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;
|
|
}
|