From 312dfed51c33c281ce671d3596ec8d46b805b0b6 Mon Sep 17 00:00:00 2001 From: Sergey Bragin Date: Wed, 2 Nov 2022 18:20:07 +0300 Subject: [PATCH] Add new widget LoadablePaginatedListBothDirectionView --- CHANGELOG.md | 5 + lib/dash_kit_core.dart | 4 +- .../loadable_list_both_direction_view.dart | 179 ++++++++++++++++++ ...le_paginated_list_both_direction_view.dart | 169 +++++++++++++++++ .../loadable_paginated_list_view.dart | 4 +- pubspec.yaml | 2 +- 6 files changed, 357 insertions(+), 6 deletions(-) create mode 100644 lib/src/loadable/loadable_list_both_direction_view.dart create mode 100644 lib/src/loadable/loadable_paginated_list_both_direction_view.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index b7af1eb..e80b27d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [3.1.10] + +- Add new widget LoadablePaginatedListBothDirectionView +- Fixed lists for using slivers + ## [3.1.9] - Fixed lists for using slivers diff --git a/lib/dash_kit_core.dart b/lib/dash_kit_core.dart index 0aeba79..93e4832 100644 --- a/lib/dash_kit_core.dart +++ b/lib/dash_kit_core.dart @@ -6,18 +6,16 @@ export 'package:built_collection/built_collection.dart'; export './src/components/action.dart'; export './src/components/global_state.dart'; export './src/components/store.dart'; - export './src/loadable/blocked_loadable_view.dart'; export './src/loadable/loadable_grid_view.dart'; export './src/loadable/loadable_item_view.dart'; export './src/loadable/loadable_list_view.dart'; export './src/loadable/loadable_paginated_grid_view.dart'; +export './src/loadable/loadable_paginated_list_both_direction_view.dart'; export './src/loadable/loadable_paginated_list_view.dart'; export './src/loadable/loadable_view.dart'; export './src/loadable/pagination_state.dart'; - export './src/states/operation_state.dart'; export './src/states/paginated_list.dart'; - export './src/utils/build_context_extensions.dart'; export './src/utils/store_list.dart'; diff --git a/lib/src/loadable/loadable_list_both_direction_view.dart b/lib/src/loadable/loadable_list_both_direction_view.dart new file mode 100644 index 0000000..953224a --- /dev/null +++ b/lib/src/loadable/loadable_list_both_direction_view.dart @@ -0,0 +1,179 @@ +import 'package:dash_kit_core/src/loadable/loadable_list_view.dart'; +import 'package:dash_kit_core/src/loadable/pagination_state.dart'; +import 'package:dash_kit_core/src/states/operation_state.dart'; +import 'package:dash_kit_core/src/utils/store_list.dart'; +import 'package:flutter/material.dart'; + +class LoadableListBothDirectionView + extends StatefulWidget { + const LoadableListBothDirectionView({ + required this.headerViewModel, + required this.footerViewModel, + required this.emptyStateWidget, + required this.errorWidget, + required this.startWidget, + this.anchor = 0.5, + this.scrollPhysics = const AlwaysScrollableScrollPhysics(), + this.onChangeContentOffset, + this.cacheExtent, + this.shrinkWrap = false, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.progressIndicator = const CircularProgressIndicator(), + this.padding, + Key? key, + }) : super(key: key); + + final LoadableListViewModel headerViewModel; + final LoadableListViewModel footerViewModel; + final double anchor; + final ScrollPhysics scrollPhysics; + final void Function(double offset)? onChangeContentOffset; + final double? cacheExtent; + final bool shrinkWrap; + final Axis scrollDirection; + final bool reverse; + final Widget progressIndicator; + final Widget emptyStateWidget; + final Widget errorWidget; + final Widget? startWidget; + final EdgeInsets? padding; + + @override + State createState() => + LoadableListBothDirectionViewState(); +} + +class LoadableListBothDirectionViewState + extends State { + final ScrollController scrollController = ScrollController(); + + LoadableListViewModel get headerViewModel => + widget.headerViewModel as LoadableListViewModel; + LoadableListViewModel get footerViewModel => + widget.footerViewModel as LoadableListViewModel; + + @override + void initState() { + super.initState(); + if (headerViewModel.loadListRequestState.isIdle) { + headerViewModel.loadList?.call(); + } + + if (footerViewModel.loadListRequestState.isIdle) { + footerViewModel.loadList?.call(); + } + + scrollController.addListener(_onScrollChanged); + } + + @override + Widget build(BuildContext context) { + final centerKey = UniqueKey(); + final headerState = headerViewModel.getPaginationState(); + final footerState = footerViewModel.getPaginationState(); + + if (headerState == PaginationState.loading || + footerState == PaginationState.loading) { + return _ProgressSate( + progressIndicator: widget.progressIndicator, + padding: widget.padding, + ); + } + + if (headerState == PaginationState.empty && + footerState == PaginationState.empty) { + return widget.emptyStateWidget; + } + + if (headerState == PaginationState.error || + footerState == PaginationState.error) { + return widget.errorWidget; + } + + return CustomScrollView( + key: widget.key, + center: centerKey, + anchor: widget.anchor, + shrinkWrap: widget.shrinkWrap, + physics: widget.scrollPhysics, + controller: scrollController, + cacheExtent: widget.cacheExtent, + scrollDirection: widget.scrollDirection, + reverse: widget.reverse, + slivers: [ + SliverPadding( + padding: widget.padding ?? EdgeInsets.zero, + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => buildListItem(index, headerViewModel), + childCount: headerViewModel.itemsCount, + ), + ), + ), + if (headerViewModel.endListWidget != null) + SliverToBoxAdapter(child: footerViewModel.endListWidget), + SliverToBoxAdapter( + key: centerKey, + child: widget.startWidget ?? const SizedBox.shrink(), + ), + SliverPadding( + padding: widget.padding ?? EdgeInsets.zero, + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => buildListItem(index, footerViewModel), + childCount: footerViewModel.itemsCount, + ), + ), + ), + if (footerViewModel.endListWidget != null) + SliverToBoxAdapter(child: footerViewModel.endListWidget), + ], + ); + } + + @override + void dispose() { + super.dispose(); + scrollController.removeListener(_onScrollChanged); + scrollController.dispose(); + } + + void _onScrollChanged() { + widget.onChangeContentOffset?.call(scrollController.position.pixels); + } + + Widget buildListItem(int index, LoadableListViewModel model) => + _ListItem(index: index, model: model); +} + +class _ProgressSate extends StatelessWidget { + const _ProgressSate({required this.progressIndicator, this.padding}); + + final Widget progressIndicator; + final EdgeInsetsGeometry? padding; + + @override + Widget build(BuildContext context) { + return Container( + padding: padding, + alignment: Alignment.center, + child: progressIndicator, + ); + } +} + +class _ListItem extends StatelessWidget { + const _ListItem({required this.index, required this.model}); + + final int index; + final LoadableListViewModel model; + + @override + Widget build(BuildContext context) => Column( + children: [ + model.itemBuilder(index), + if (index != model.itemsCount - 1) model.itemSeparator(index), + ], + ); +} diff --git a/lib/src/loadable/loadable_paginated_list_both_direction_view.dart b/lib/src/loadable/loadable_paginated_list_both_direction_view.dart new file mode 100644 index 0000000..60a9ae3 --- /dev/null +++ b/lib/src/loadable/loadable_paginated_list_both_direction_view.dart @@ -0,0 +1,169 @@ +import 'package:dash_kit_core/dash_kit_core.dart'; +import 'package:dash_kit_core/src/loadable/loadable_list_both_direction_view.dart'; +import 'package:flutter/material.dart'; + +class LoadablePaginatedListBothDirectionView + extends LoadableListBothDirectionView { + const LoadablePaginatedListBothDirectionView({ + required LoadablePaginatedListViewModel headerViewModel, + required LoadablePaginatedListViewModel footerViewModel, + required Widget emptyStateWidget, + required Widget errorWidget, + Key? key, + ScrollPhysics scrollPhysics = const AlwaysScrollableScrollPhysics(), + double? cacheExtent, + bool shrinkWrap = false, + Axis scrollDirection = Axis.vertical, + bool reverse = false, + void Function(double offset)? onChangeContentOffset, + Widget progressIndicator = const CircularProgressIndicator(), + Widget? startWidget, + double anchor = 0.5, + }) : super( + key: key, + headerViewModel: headerViewModel, + footerViewModel: footerViewModel, + scrollPhysics: scrollPhysics, + onChangeContentOffset: onChangeContentOffset, + cacheExtent: cacheExtent, + shrinkWrap: shrinkWrap, + scrollDirection: scrollDirection, + reverse: reverse, + progressIndicator: progressIndicator, + emptyStateWidget: emptyStateWidget, + errorWidget: errorWidget, + startWidget: startWidget, + anchor: anchor, + ); + + @override + State createState() => + LoadablePaginatedListBothDirectionState(); +} + +class LoadablePaginatedListBothDirectionState + extends LoadableListBothDirectionViewState { + @override + LoadablePaginatedListViewModel get headerViewModel => + widget.headerViewModel as LoadablePaginatedListViewModel; + @override + LoadablePaginatedListViewModel get footerViewModel => + widget.footerViewModel as LoadablePaginatedListViewModel; + + @override + void initState() { + super.initState(); + + scrollController.addListener(_onScrollChanged); + } + + @override + Widget buildListItem(int index, LoadableListViewModel model) { + return index == model.itemsCount - 1 + ? _LastItem( + paginatedModel: model as LoadablePaginatedListViewModel, + scrollController: scrollController, + progressIndicator: widget.progressIndicator, + cacheExtent: widget.cacheExtent, + ) + : super.buildListItem(index, model); + } + + @override + void dispose() { + super.dispose(); + scrollController.removeListener(_onScrollChanged); + } + + void _onScrollChanged() { + final canLoadHeader = (headerViewModel.loadPageRequestState.isSucceed || + headerViewModel.loadPageRequestState.isIdle) && + !headerViewModel.paginatedList.isAllItemsLoaded; + final canLoadFooter = (footerViewModel.loadPageRequestState.isSucceed || + footerViewModel.loadPageRequestState.isIdle) && + !footerViewModel.paginatedList.isAllItemsLoaded; + final maxScrollExtent = + scrollController.position.maxScrollExtent - (widget.cacheExtent ?? 0); + final minScrollExtent = + scrollController.position.minScrollExtent + (widget.cacheExtent ?? 0); + + if (scrollController.position.pixels >= maxScrollExtent && canLoadFooter) { + footerViewModel.loadPage?.call(); + } + if (scrollController.position.pixels <= minScrollExtent && canLoadHeader) { + headerViewModel.loadPage?.call(); + } + } +} + +class _LastItem extends StatelessWidget { + const _LastItem({ + required this.paginatedModel, + required this.scrollController, + required this.progressIndicator, + this.cacheExtent, + }); + + final LoadablePaginatedListViewModel paginatedModel; + final ScrollController scrollController; + final Widget progressIndicator; + final double? cacheExtent; + + @override + Widget build(BuildContext context) { + switch (paginatedModel.getPaginationState()) { + case PaginationState.loadingPage: + return _ProgressPage( + scrollController: scrollController, + progressIndicator: progressIndicator, + cacheExtent: cacheExtent, + ); + + case PaginationState.errorLoadingPage: + return paginatedModel.errorWidget; + + case PaginationState.succeedLoadingPage: + if (paginatedModel.paginatedList.isAllItemsLoaded) { + return paginatedModel.endListWidget ?? const SizedBox.shrink(); + } + + return const SizedBox.shrink(); + + default: + return const SizedBox.shrink(); + } + } +} + +class _ProgressPage extends StatelessWidget { + const _ProgressPage({ + required this.progressIndicator, + required this.scrollController, + this.cacheExtent, + }); + + final Widget progressIndicator; + final ScrollController scrollController; + final double? cacheExtent; + + @override + Widget build(BuildContext context) { + { + WidgetsBinding.instance.addPostFrameCallback((_) => cacheExtent == null + ? scrollController.animateTo( + scrollController.offset > 0 + ? scrollController.position.maxScrollExtent + : scrollController.position.minScrollExtent, + duration: const Duration(milliseconds: 100), + curve: Curves.linear, + ) + : null); + + return Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.only(top: 8), + child: Center(child: progressIndicator), + ); + } + } +} diff --git a/lib/src/loadable/loadable_paginated_list_view.dart b/lib/src/loadable/loadable_paginated_list_view.dart index c812faf..80b900b 100644 --- a/lib/src/loadable/loadable_paginated_list_view.dart +++ b/lib/src/loadable/loadable_paginated_list_view.dart @@ -101,11 +101,11 @@ class LoadablePaginatedListState void _onScrollChanged() { final canLoad = (viewModel.loadPageRequestState.isSucceed || viewModel.loadPageRequestState.isIdle) && - viewModel.paginatedList.isAllItemsLoaded == false; + !viewModel.paginatedList.isAllItemsLoaded; final maxScrollExtent = scrollController.position.maxScrollExtent - (widget.cacheExtent ?? 0); - if (scrollController.position.pixels > maxScrollExtent && canLoad) { + if (scrollController.position.pixels >= maxScrollExtent && canLoad) { viewModel.loadPage?.call(); } } diff --git a/pubspec.yaml b/pubspec.yaml index 3204c99..21dbab5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: dash_kit_core description: The core component for DashKit that provides basic architecture components -version: 3.1.9 +version: 3.1.10 homepage: https://github.com/Dash-Kit/dash-kit-core environment: