Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## [3.1.10]

- Add new widget LoadablePaginatedListBothDirectionView
- Fixed lists for using slivers

## [3.1.9]

- Fixed lists for using slivers
Expand Down
4 changes: 1 addition & 3 deletions lib/dash_kit_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
179 changes: 179 additions & 0 deletions lib/src/loadable/loadable_list_both_direction_view.dart
Original file line number Diff line number Diff line change
@@ -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<T extends StoreListItem>
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<T> headerViewModel;
final LoadableListViewModel<T> 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<StatefulWidget> createState() =>
LoadableListBothDirectionViewState<T>();
}

class LoadableListBothDirectionViewState<T extends StoreListItem>
extends State<LoadableListBothDirectionView> {
final ScrollController scrollController = ScrollController();

LoadableListViewModel<T> get headerViewModel =>
widget.headerViewModel as LoadableListViewModel<T>;
LoadableListViewModel<T> get footerViewModel =>
widget.footerViewModel as LoadableListViewModel<T>;

@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),
],
);
}
169 changes: 169 additions & 0 deletions lib/src/loadable/loadable_paginated_list_both_direction_view.dart
Original file line number Diff line number Diff line change
@@ -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<T extends StoreListItem>
extends LoadableListBothDirectionView<T> {
const LoadablePaginatedListBothDirectionView({
required LoadablePaginatedListViewModel<T> headerViewModel,
required LoadablePaginatedListViewModel<T> 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<StatefulWidget> createState() =>
LoadablePaginatedListBothDirectionState<T>();
}

class LoadablePaginatedListBothDirectionState<T extends StoreListItem>
extends LoadableListBothDirectionViewState<T> {
@override
LoadablePaginatedListViewModel<T> get headerViewModel =>
widget.headerViewModel as LoadablePaginatedListViewModel<T>;
@override
LoadablePaginatedListViewModel<T> get footerViewModel =>
widget.footerViewModel as LoadablePaginatedListViewModel<T>;

@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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be in initState

? 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),
);
}
}
}
4 changes: 2 additions & 2 deletions lib/src/loadable/loadable_paginated_list_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,11 @@ class LoadablePaginatedListState<T extends StoreListItem>
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();
}
}
Expand Down
Loading