feat : build list view widget

This commit is contained in:
2025-06-28 16:45:28 +03:30
parent dd4046cd35
commit 5d2e767f99
7 changed files with 437 additions and 1 deletions

View File

@@ -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,

View File

@@ -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<T> 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<List<T>> resource;
final Future<void> 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,
});
}

View File

@@ -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<T> 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<List<T>> resource;
final NullableIndexedWidgetBuilder itemBuilder;
final IndexedWidgetBuilder? separatorBuilder;
final Future<void> Function()? onRefresh;
final Future<void> 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<ScrollNotification>(
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);
},
),
),
);
}
}

View File

@@ -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,
),
);
}
}

View File

@@ -0,0 +1,64 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:rasadyar_core/utils/network/resource.dart';
mixin PaginationControllerMixin<T> on GetxController {
final Rx<Resource<List<T>>> resource = Resource<List<T>>.initial().obs;
final RxBool isPaginating = false.obs;
final RxBool hasMore = true.obs;
int currentPage = 1;
Timer? _debounceTimer;
Future<void> Function()? _lastTriedOperation;
Future<List<T>> fetchPage(int page);
void retryLastOperation() => _lastTriedOperation?.call();
Future<void> 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<void> 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<T>.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;
}
});
}
}

View File

@@ -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

View File

@@ -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