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

610 lines
18 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:rx_bloc/rx_bloc.dart';
import 'package:rx_bloc_list/rx_bloc_list.dart';
import 'package:rxdart/rxdart.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});