From 5d2e767f99414060b288190f17f64381aa5a85a2 Mon Sep 17 00:00:00 2001 From: "mr.mojtaba" Date: Sat, 28 Jun 2025 16:45:28 +0330 Subject: [PATCH] feat : build list view widget --- .../pages/sales_in_province/logic.dart | 2 +- .../widget/list_view/r_list_view.dart | 219 ++++++++++++++++++ .../list_view/r_paginated_list_view.dart | 97 ++++++++ .../widget/list_view/r_shimmer_list.dart | 45 ++++ .../mixins/pagination_controller_mixin.dart | 64 +++++ packages/core/pubspec.lock | 8 + packages/core/pubspec.yaml | 3 + 7 files changed, 437 insertions(+), 1 deletion(-) create mode 100644 packages/core/lib/presentation/widget/list_view/r_list_view.dart create mode 100644 packages/core/lib/presentation/widget/list_view/r_paginated_list_view.dart create mode 100644 packages/core/lib/presentation/widget/list_view/r_shimmer_list.dart create mode 100644 packages/core/lib/utils/mixins/pagination_controller_mixin.dart diff --git a/packages/chicken/lib/presentation/pages/sales_in_province/logic.dart b/packages/chicken/lib/presentation/pages/sales_in_province/logic.dart index 8451ba1..7c43a7c 100644 --- a/packages/chicken/lib/presentation/pages/sales_in_province/logic.dart +++ b/packages/chicken/lib/presentation/pages/sales_in_province/logic.dart @@ -240,7 +240,7 @@ class SalesInProvinceLogic extends GetxController { amount: pricePerKilo.value, totalAmount: totalCost.value, weightOfCarcasses: weight.value, - sellType:saleType.value ==2 ? "free" :'exclusive', + sellType: saleType.value == 2 ? "free" : 'exclusive', numberOfCarcasses: 0, guildKey: selectedGuildModel.value?.key, productKey: selectedProductModel.value?.key, diff --git a/packages/core/lib/presentation/widget/list_view/r_list_view.dart b/packages/core/lib/presentation/widget/list_view/r_list_view.dart new file mode 100644 index 0000000..9cc1a95 --- /dev/null +++ b/packages/core/lib/presentation/widget/list_view/r_list_view.dart @@ -0,0 +1,219 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:rasadyar_core/utils/network/resource.dart'; + +import 'r_shimmer_list.dart'; + +enum ListType { builder, separated } + +class RListView extends StatelessWidget { + final ListType type; + final Axis scrollDirection; + final bool reverse; + final ScrollController? controller; + final bool? primary; + final ScrollPhysics? physics; + final ScrollBehavior? scrollBehavior; + final bool shrinkWrap; + final Key? center; + final double? cacheExtent; + final int? semanticChildCount; + final int itemCount; + final DragStartBehavior dragStartBehavior; + final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; + final String? restorationId; + final Clip clipBehavior; + final HitTestBehavior hitTestBehavior; + final Widget? prototypeItem; + final EdgeInsetsGeometry? padding; + final double? itemExtent; + final ItemExtentBuilder? itemExtentBuilder; + final ChildIndexGetter? findChildIndexCallback; + final NullableIndexedWidgetBuilder itemBuilder; + final IndexedWidgetBuilder? separatorBuilder; + final bool addAutomaticKeepAlives; + final bool addRepaintBoundaries; + final bool addSemanticIndexes; + final Widget loadingWidget; + final Widget emptyWidget; + final Widget errorWidget; + final Resource> resource; + final Future Function()? onRefresh; + + const RListView.builder({ + super.key, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.controller, + this.primary, + this.physics, + this.scrollBehavior, + this.shrinkWrap = false, + this.center, + this.cacheExtent, + this.semanticChildCount, + required this.itemCount, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + this.hitTestBehavior = HitTestBehavior.opaque, + this.prototypeItem, + this.padding, + this.itemExtent, + this.itemExtentBuilder, + this.findChildIndexCallback, + required this.itemBuilder, + this.addAutomaticKeepAlives = true, + this.addRepaintBoundaries = true, + this.addSemanticIndexes = true, + this.loadingWidget = const RShimmerList(isSeparated: true), + this.emptyWidget = const Center(child: Text("هیچ آیتمی یافت نشد")), + this.errorWidget = const Center(child: CircularProgressIndicator()), + required this.resource, + this.onRefresh, + }) : type = ListType.builder, + separatorBuilder = null; + + const RListView.separated({ + super.key, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.controller, + this.primary, + this.physics, + this.scrollBehavior, + this.shrinkWrap = false, + this.center, + this.cacheExtent, + this.semanticChildCount, + required this.itemCount, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + this.hitTestBehavior = HitTestBehavior.opaque, + this.prototypeItem, + this.padding, + this.itemExtent, + this.itemExtentBuilder, + this.findChildIndexCallback, + required this.itemBuilder, + required this.separatorBuilder, + this.addAutomaticKeepAlives = true, + this.addRepaintBoundaries = true, + this.addSemanticIndexes = true, + this.loadingWidget = const Center(child: CircularProgressIndicator()), + this.emptyWidget = const Center(child: Text("هیچ آیتمی یافت نشد")), + this.errorWidget = const Center(child: CircularProgressIndicator()), + required this.resource, + this.onRefresh, + }) : type = ListType.separated; + + @override + Widget build(BuildContext context) { + switch (resource.status) { + case Status.initial: + case Status.loading: + return loadingWidget; + + case Status.error: + return errorWidget; + + case Status.empty: + return emptyWidget; + + case Status.success: + if (resource.data?.isEmpty ?? true) { + return emptyWidget; + } + + final list = type == ListType.builder + ? ListView.builder( + key: key, + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + itemExtent: itemExtent, + itemBuilder: itemBuilder, + itemCount: itemCount, + prototypeItem: prototypeItem, + itemExtentBuilder: itemExtentBuilder, + findChildIndexCallback: findChildIndexCallback, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + cacheExtent: cacheExtent, + semanticChildCount: semanticChildCount, + dragStartBehavior: dragStartBehavior, + keyboardDismissBehavior: keyboardDismissBehavior, + restorationId: restorationId, + clipBehavior: clipBehavior, + ) + : ListView.separated( + key: key, + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + itemBuilder: itemBuilder, + separatorBuilder: separatorBuilder!, + itemCount: itemCount, + findChildIndexCallback: findChildIndexCallback, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + cacheExtent: cacheExtent, + dragStartBehavior: dragStartBehavior, + keyboardDismissBehavior: keyboardDismissBehavior, + restorationId: restorationId, + clipBehavior: clipBehavior, + ); + + return onRefresh != null ? RefreshIndicator(onRefresh: onRefresh!, child: list) : list; + } + } + + RListView._({ + required this.type, + required this.scrollDirection, + required this.reverse, + required this.controller, + required this.primary, + required this.physics, + required this.scrollBehavior, + required this.shrinkWrap, + required this.center, + required this.cacheExtent, + required this.semanticChildCount, + required this.itemCount, + required this.dragStartBehavior, + required this.keyboardDismissBehavior, + required this.restorationId, + required this.clipBehavior, + required this.hitTestBehavior, + required this.prototypeItem, + required this.padding, + required this.itemExtent, + required this.itemExtentBuilder, + required this.findChildIndexCallback, + required this.itemBuilder, + required this.separatorBuilder, + required this.addAutomaticKeepAlives, + required this.addRepaintBoundaries, + required this.addSemanticIndexes, + required this.loadingWidget, + required this.emptyWidget, + required this.errorWidget, + required this.resource, + required this.onRefresh, + }); +} diff --git a/packages/core/lib/presentation/widget/list_view/r_paginated_list_view.dart b/packages/core/lib/presentation/widget/list_view/r_paginated_list_view.dart new file mode 100644 index 0000000..52cdd93 --- /dev/null +++ b/packages/core/lib/presentation/widget/list_view/r_paginated_list_view.dart @@ -0,0 +1,97 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:rasadyar_core/utils/network/resource.dart'; + +import 'r_shimmer_list.dart'; + +enum ListType { builder, separated } + +class RPaginatedListView extends StatelessWidget { + const RPaginatedListView({ + super.key, + required this.resource, + required this.itemBuilder, + required this.itemCount, + this.separatorBuilder, + this.onRefresh, + required this.onLoadMore, + this.isPaginating = false, + this.hasMore = true, + this.loadingWidget, + this.emptyWidget, + this.errorWidget, + this.scrollController, + this.listType = ListType.builder, + }); + + final Resource> resource; + final NullableIndexedWidgetBuilder itemBuilder; + final IndexedWidgetBuilder? separatorBuilder; + final Future Function()? onRefresh; + final Future Function() onLoadMore; + final bool isPaginating; + final bool hasMore; + final int itemCount; + final Widget? loadingWidget; + final Widget? emptyWidget; + final Widget? errorWidget; + final ScrollController? scrollController; + final ListType listType; + + @override + Widget build(BuildContext context) { + if (resource.isLoading) { + return loadingWidget ?? RShimmerList(isSeparated: listType == ListType.separated); + } + + if (resource.isError) { + return errorWidget ?? Center(child: Text(resource.message ?? 'خطا')); + } + + if (resource.isEmpty || resource.data?.isEmpty == true) { + return emptyWidget ?? const Center(child: Text('آیتمی یافت نشد')); + } + + final controller = scrollController ?? ScrollController(); + + return NotificationListener( + onNotification: (ScrollNotification scrollInfo) { + if (!isPaginating && hasMore && scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent - 100) { + onLoadMore(); + } + return false; + }, + child: RefreshIndicator( + onRefresh: onRefresh ?? () async {}, + child: listType == ListType.separated + ? ListView.separated( + controller: controller, + itemCount: itemCount + (isPaginating ? 1 : 0), + itemBuilder: (context, index) { + if (isPaginating && index == itemCount) { + return const Padding( + padding: EdgeInsets.all(16), + child: Center(child: CupertinoActivityIndicator()), + ); + } + return itemBuilder(context, index); + }, + separatorBuilder: separatorBuilder ?? (_, __) => const SizedBox(height: 8), + ) + : ListView.builder( + controller: controller, + itemCount: itemCount + (isPaginating ? 1 : 0), + itemBuilder: (context, index) { + if (isPaginating && index == itemCount) { + return const Padding( + padding: EdgeInsets.all(16), + child: Center(child: CupertinoActivityIndicator()), + ); + } + return itemBuilder(context, index); + }, + ), + ), + ); + } +} diff --git a/packages/core/lib/presentation/widget/list_view/r_shimmer_list.dart b/packages/core/lib/presentation/widget/list_view/r_shimmer_list.dart new file mode 100644 index 0000000..c6fd4e9 --- /dev/null +++ b/packages/core/lib/presentation/widget/list_view/r_shimmer_list.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +class RShimmerList extends StatelessWidget { + const RShimmerList({super.key, this.itemCount = 6, this.itemBuilder, this.height = 80, this.isSeparated = false}); + + final int itemCount; + final double height; + final bool isSeparated; + final IndexedWidgetBuilder? itemBuilder; + + @override + Widget build(BuildContext context) { + final builder = + itemBuilder ?? + (_, __) => Container( + height: height, + margin: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(8)), + ); + + final children = List.generate(itemCount, (index) => builder(context, index)); + + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: isSeparated + ? ListView.separated( + itemCount: itemCount, + padding: const EdgeInsets.all(12), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: builder, + separatorBuilder: (_, __) => const SizedBox(height: 8), + ) + : ListView.builder( + itemCount: itemCount, + padding: const EdgeInsets.all(12), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: builder, + ), + ); + } +} diff --git a/packages/core/lib/utils/mixins/pagination_controller_mixin.dart b/packages/core/lib/utils/mixins/pagination_controller_mixin.dart new file mode 100644 index 0000000..c84b88e --- /dev/null +++ b/packages/core/lib/utils/mixins/pagination_controller_mixin.dart @@ -0,0 +1,64 @@ +import 'dart:async'; + +import 'package:get/get.dart'; +import 'package:rasadyar_core/utils/network/resource.dart'; + +mixin PaginationControllerMixin on GetxController { + final Rx>> resource = Resource>.initial().obs; + final RxBool isPaginating = false.obs; + final RxBool hasMore = true.obs; + int currentPage = 1; + + Timer? _debounceTimer; + Future Function()? _lastTriedOperation; + + Future> fetchPage(int page); + + void retryLastOperation() => _lastTriedOperation?.call(); + + Future refreshData() async { + _lastTriedOperation = refreshData; + try { + currentPage = 1; + resource.value = const Resource.loading(); + final items = await fetchPage(currentPage); + if (items.isEmpty) { + resource.value = const Resource.empty(); + hasMore.value = false; + } else { + resource.value = Resource.success(items); + hasMore.value = true; + } + } catch (e) { + resource.value = Resource.error(e.toString()); + } + } + + Future loadMoreData() async { + _lastTriedOperation = loadMoreData; + + if (_debounceTimer?.isActive ?? false) return; + + _debounceTimer = Timer(const Duration(milliseconds: 300), () async { + if (isPaginating.value || !hasMore.value) return; + + try { + isPaginating.value = true; + final nextPage = currentPage + 1; + final newItems = await fetchPage(nextPage); + if (newItems.isEmpty) { + hasMore.value = false; + } else { + final currentList = List.from(resource.value.data ?? []); + currentList.addAll(newItems); + resource.value = Resource.success(currentList); + currentPage = nextPage; + } + } catch (_) { + // ignore or optionally handle pagination error + } finally { + isPaginating.value = false; + } + }); + } +} diff --git a/packages/core/pubspec.lock b/packages/core/pubspec.lock index 8b1d774..f60f4b9 100644 --- a/packages/core/pubspec.lock +++ b/packages/core/pubspec.lock @@ -1133,6 +1133,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter diff --git a/packages/core/pubspec.yaml b/packages/core/pubspec.yaml index 76edb96..0961bfd 100644 --- a/packages/core/pubspec.yaml +++ b/packages/core/pubspec.yaml @@ -42,6 +42,9 @@ dependencies: flutter_svg: ^2.0.17 font_awesome_flutter: ^10.8.0 + #Shimmer + shimmer: ^3.0.0 + #Generator flutter_gen_runner: ^5.10.0