638 lines
18 KiB
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();
|
|
}
|
|
}
|