feat : build list view widget
This commit is contained in:
@@ -240,7 +240,7 @@ class SalesInProvinceLogic extends GetxController {
|
|||||||
amount: pricePerKilo.value,
|
amount: pricePerKilo.value,
|
||||||
totalAmount: totalCost.value,
|
totalAmount: totalCost.value,
|
||||||
weightOfCarcasses: weight.value,
|
weightOfCarcasses: weight.value,
|
||||||
sellType:saleType.value ==2 ? "free" :'exclusive',
|
sellType: saleType.value == 2 ? "free" : 'exclusive',
|
||||||
numberOfCarcasses: 0,
|
numberOfCarcasses: 0,
|
||||||
guildKey: selectedGuildModel.value?.key,
|
guildKey: selectedGuildModel.value?.key,
|
||||||
productKey: selectedProductModel.value?.key,
|
productKey: selectedProductModel.value?.key,
|
||||||
|
|||||||
219
packages/core/lib/presentation/widget/list_view/r_list_view.dart
Normal file
219
packages/core/lib/presentation/widget/list_view/r_list_view.dart
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1133,6 +1133,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
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:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ dependencies:
|
|||||||
flutter_svg: ^2.0.17
|
flutter_svg: ^2.0.17
|
||||||
font_awesome_flutter: ^10.8.0
|
font_awesome_flutter: ^10.8.0
|
||||||
|
|
||||||
|
#Shimmer
|
||||||
|
shimmer: ^3.0.0
|
||||||
|
|
||||||
#Generator
|
#Generator
|
||||||
flutter_gen_runner: ^5.10.0
|
flutter_gen_runner: ^5.10.0
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user