605 lines
18 KiB
Dart
605 lines
18 KiB
Dart
part of 'widgets.dart';
|
||
|
||
/// [TypeDefs] ----------------------------------------------------
|
||
|
||
typedef EnhancedListViewKey<BodyType, HeaderType, FooterType, QueryType>
|
||
= GlobalKey<
|
||
EnhancedListViewState<BodyType, HeaderType, FooterType, QueryType>>;
|
||
typedef HeaderTileBuilder<HeaderType> = Widget Function(
|
||
Future<List<HeaderType?>> Function() headerItems);
|
||
typedef BodyTileBuilder<BodyType> = Widget Function(
|
||
BuildContext context, BodyType item, int index);
|
||
typedef FooterTileBuilder<FooterType> = Widget Function(
|
||
Future<List<FooterType?>> Function() footerItems);
|
||
typedef Query<QueryType> = QueryType?;
|
||
typedef BodyRetrievalUseCase<BodyType, QueryType> = Future<List<BodyType?>>
|
||
Function(int page, int pageSize, QueryType query);
|
||
typedef HeaderRetrievalUseCase<HeaderType> = Future<List<HeaderType?>>
|
||
Function();
|
||
typedef FooterRetrievalUseCase<FooterType> = Future<List<FooterType?>>
|
||
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();
|
||
// });
|
||
}
|
||
|
||
/// [Interfaces] ----------------------------------------------------
|
||
|
||
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<BodyType, HeaderType, FooterType,
|
||
QueryType> extends StatefulWidget {
|
||
const EnhancedListViewBase({super.key});
|
||
}
|
||
|
||
abstract interface class EnhancedListViewBaseState<BodyType, HeaderType,
|
||
FooterType, QueryType>
|
||
extends State<
|
||
EnhancedListViewBase<BodyType, HeaderType, FooterType, QueryType>> {}
|
||
|
||
/// [Mixins] ----------------------------------------------------
|
||
|
||
mixin EnhancedListViewMixin<BodyType, HeaderType, FooterType, QueryType> {
|
||
late EnhancedListViewBloc<BodyType, HeaderType, FooterType, QueryType> 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<BodyType, HeaderType, FooterType, QueryType>
|
||
extends EnhancedListViewBase<BodyType, HeaderType, FooterType, QueryType> {
|
||
final EnhancedListViewRepository<BodyType, HeaderType, FooterType, QueryType>
|
||
repository;
|
||
final EnhancedListViewController<BodyType, HeaderType, FooterType, QueryType>
|
||
controller;
|
||
|
||
const EnhancedListView({
|
||
Key? key,
|
||
required this.repository,
|
||
required this.controller,
|
||
}) : 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>>
|
||
with EnhancedListViewMixin<ItemType, HeaderType, FooterType, QueryType> {
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
|
||
bloc = EnhancedListViewBloc<ItemType, HeaderType, FooterType, QueryType>(
|
||
bodyItemsBuilder: widget.repository.fetchBody,
|
||
headerItemsBuilder: widget.repository.fetchHeader ?? () async => [],
|
||
footerItemsBuilder: widget.repository.fetchFooter ?? () async => [],
|
||
);
|
||
bloc.events.loadBodyItems();
|
||
bloc.events.loadHeaderItems();
|
||
bloc.events.loadFooterItems();
|
||
}
|
||
|
||
@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
|
||
.controller.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
|
||
.controller.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.controller
|
||
.bodyBuilder(context, bodySnapshot.data![index], index);
|
||
},
|
||
);
|
||
}
|
||
},
|
||
),
|
||
);
|
||
|
||
return Column(
|
||
children: [
|
||
if (widget.controller.headerBuilder != null) header,
|
||
body,
|
||
if (widget.controller.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);
|
||
}
|
||
|
||
/// [Repositories] ----------------------------------------------------
|
||
|
||
class EnhancedListViewRepository<BodyType, HeaderType, FooterType, QueryType> {
|
||
final BodyRetrievalUseCase<BodyType, QueryType?> fetchBody;
|
||
final HeaderRetrievalUseCase<HeaderType>? fetchHeader;
|
||
final FooterRetrievalUseCase<FooterType>? fetchFooter;
|
||
|
||
const EnhancedListViewRepository({
|
||
required this.fetchBody,
|
||
this.fetchHeader,
|
||
this.fetchFooter,
|
||
});
|
||
}
|
||
|
||
// class HeaderRepository<HeaderType, QueryType> {}
|
||
// class BodyRepository<BodyType, QueryType> {}
|
||
// class FooterRepository<FooterType, QueryType> {}
|
||
|
||
/// [Controllers] ----------------------------------------------------
|
||
|
||
class EnhancedListViewController<BodyType, HeaderType, FooterType, QueryType> {
|
||
final BodyTileBuilder<BodyType> bodyBuilder;
|
||
final HeaderTileBuilder<HeaderType>? headerBuilder;
|
||
final FooterTileBuilder<FooterType>? footerBuilder;
|
||
|
||
const EnhancedListViewController({
|
||
required this.bodyBuilder,
|
||
this.headerBuilder,
|
||
this.footerBuilder,
|
||
});
|
||
}
|
||
|
||
// class HeaderController<HeaderType, QueryType> {}
|
||
// class BodyController<BodyType, QueryType> {}
|
||
// class FooterController<FooterType, QueryType> {}
|
||
|
||
/// [Blocs] ----------------------------------------------------
|
||
|
||
Stream<bool> get loadingState => Stream<bool>.empty();
|
||
Stream<Exception> get errorState => Stream<Exception>.empty();
|
||
|
||
abstract class EnhancedListViewEvents<BodyType, HeaderType, FooterType,
|
||
QueryType> {
|
||
void loadBodyItems({bool reset = false, dynamic query = null});
|
||
void loadHeaderItems();
|
||
void loadFooterItems();
|
||
}
|
||
|
||
abstract class EnhancedListViewStates<BodyType, HeaderType, FooterType,
|
||
QueryType> {
|
||
Stream<List<BodyType>> get bodyItems;
|
||
Stream<List<HeaderType>> get headerItems;
|
||
Stream<List<FooterType>> get footerItems;
|
||
Stream<bool> get isLoading;
|
||
Stream<String> get errors;
|
||
}
|
||
|
||
class EnhancedListViewBloc<BodyType, HeaderType, FooterType, QueryType>
|
||
extends $EnhancedListViewBloc<BodyType, HeaderType, FooterType,
|
||
QueryType?> {
|
||
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 BodyRetrievalUseCase<BodyType, QueryType?> bodyItemsBuilder;
|
||
final HeaderRetrievalUseCase<HeaderType> headerItemsBuilder;
|
||
final FooterRetrievalUseCase<FooterType> footerItemsBuilder;
|
||
|
||
final _bodyItems = BehaviorSubject<List<BodyType>>.seeded([]);
|
||
|
||
final _headerItems = BehaviorSubject<List<HeaderType>>.seeded([]);
|
||
|
||
final _footerItems = BehaviorSubject<List<FooterType>>.seeded([]);
|
||
|
||
final _isLoading = BehaviorSubject<bool>.seeded(false);
|
||
final _errors = BehaviorSubject<String>();
|
||
|
||
Stream<List<BodyType>> _fetchBodyItems(bool reset, QueryType? query) async* {
|
||
try {
|
||
_isLoading.add(true);
|
||
final items = await bodyItemsBuilder(1, 10, query);
|
||
yield items.whereType<BodyType>().toList();
|
||
} catch (e) {
|
||
_errors.add(e.toString());
|
||
} finally {
|
||
_isLoading.add(false);
|
||
}
|
||
}
|
||
|
||
Stream<List<HeaderType>> _fetchHeaderItems() async* {
|
||
try {
|
||
_isLoading.add(true);
|
||
final items = await headerItemsBuilder();
|
||
yield items.whereType<HeaderType>().toList();
|
||
} catch (e) {
|
||
_errors.add(e.toString());
|
||
} finally {
|
||
_isLoading.add(false);
|
||
}
|
||
}
|
||
|
||
Stream<List<FooterType>> _fetchFooterItems() async* {
|
||
try {
|
||
_isLoading.add(true);
|
||
final items = await footerItemsBuilder();
|
||
yield items.whereType<FooterType>().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<List<BodyType>> _mapToBodyItemsState() => _bodyItems.stream;
|
||
@override
|
||
Stream<String> _mapToErrorsState() => _errors.stream;
|
||
@override
|
||
Stream<List<FooterType>> _mapToFooterItemsState() => _footerItems.stream;
|
||
@override
|
||
Stream<List<HeaderType>> _mapToHeaderItemsState() => _headerItems.stream;
|
||
@override
|
||
Stream<bool> _mapToIsLoadingState() => _isLoading.stream;
|
||
}
|
||
|
||
abstract class EnhancedListViewBlocType extends RxBlocTypeBase {
|
||
EnhancedListViewEvents get events;
|
||
EnhancedListViewStates get states;
|
||
}
|
||
|
||
abstract class $EnhancedListViewBloc<BodyType, HeaderType, FooterType,
|
||
QueryType> 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<void>();
|
||
|
||
/// Тhe [Subject] where events sink to by calling [loadFooterItems]
|
||
final _$loadFooterItemsEvent = PublishSubject<void>();
|
||
|
||
/// The state of [bodyItems] implemented in [_mapToBodyItemsState]
|
||
late final Stream<List<BodyType>> _bodyItemsState = _mapToBodyItemsState();
|
||
|
||
/// The state of [headerItems] implemented in [_mapToHeaderItemsState]
|
||
late final Stream<List<HeaderType>> _headerItemsState =
|
||
_mapToHeaderItemsState();
|
||
|
||
/// The state of [footerItems] implemented in [_mapToFooterItemsState]
|
||
late final Stream<List<FooterType>> _footerItemsState =
|
||
_mapToFooterItemsState();
|
||
|
||
/// The state of [isLoading] implemented in [_mapToIsLoadingState]
|
||
late final Stream<bool> _isLoadingState = _mapToIsLoadingState();
|
||
|
||
/// The state of [errors] implemented in [_mapToErrorsState]
|
||
late final Stream<String> _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<List<BodyType>> get bodyItems => _bodyItemsState;
|
||
|
||
@override
|
||
Stream<List<HeaderType>> get headerItems => _headerItemsState;
|
||
|
||
@override
|
||
Stream<List<FooterType>> get footerItems => _footerItemsState;
|
||
|
||
@override
|
||
Stream<bool> get isLoading => _isLoadingState;
|
||
|
||
@override
|
||
Stream<String> get errors => _errorsState;
|
||
|
||
Stream<List<BodyType>> _mapToBodyItemsState();
|
||
|
||
Stream<List<HeaderType>> _mapToHeaderItemsState();
|
||
|
||
Stream<List<FooterType>> _mapToFooterItemsState();
|
||
|
||
Stream<bool> _mapToIsLoadingState();
|
||
|
||
Stream<String> _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});
|