feat : build list view widget
This commit is contained in:
@@ -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,
|
||||
|
||||
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"
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user