feat: product scroll infinity

This commit is contained in:
efrilm 2025-08-05 19:48:32 +07:00
parent bb9aef55cf
commit 3e9b20f237
6 changed files with 692 additions and 194 deletions

View File

@ -13,17 +13,27 @@ import 'auth_local_datasource.dart';
class ProductRemoteDatasource {
final Dio dio = DioClient.instance;
Future<Either<String, ProductResponseModel>> getProducts() async {
Future<Either<String, ProductResponseModel>> getProducts({
int page = 1,
int limit = Variables.defaultLimit,
String? categoryId,
}) async {
try {
final authData = await AuthLocalDataSource().getAuthData();
final url = '${Variables.baseUrl}/api/v1/products';
Map<String, dynamic> queryParameters = {
'page': page,
'limit': limit,
};
if (categoryId != null) {
queryParameters['category_id'] = categoryId;
}
final response = await dio.get(
url,
queryParameters: {
'page': 1,
'limit': 30,
},
queryParameters: queryParameters,
options: Options(
headers: {
'Authorization': 'Bearer ${authData.token}',

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:enaklo_pos/data/datasources/product_remote_datasource.dart';
import 'package:enaklo_pos/data/models/response/product_response_model.dart';
@ -9,15 +11,136 @@ part 'product_loader_bloc.freezed.dart';
class ProductLoaderBloc extends Bloc<ProductLoaderEvent, ProductLoaderState> {
final ProductRemoteDatasource _productRemoteDatasource;
// Debouncing untuk mencegah multiple load more calls
Timer? _loadMoreDebounce;
bool _isLoadingMore = false;
ProductLoaderBloc(this._productRemoteDatasource)
: super(ProductLoaderState.initial()) {
on<_GetProduct>((event, emit) async {
emit(const _Loading());
final result = await _productRemoteDatasource.getProducts();
result.fold(
(l) => emit(_Error(l)),
(r) => emit(_Loaded(r.data?.products ?? [])),
on<_GetProduct>(_onGetProduct);
on<_LoadMore>(_onLoadMore);
on<_Refresh>(_onRefresh);
}
@override
Future<void> close() {
_loadMoreDebounce?.cancel();
return super.close();
}
// Debounce transformer untuk load more
// EventTransformer<T> _debounceTransformer<T>() {
// return (events, mapper) {
// return events
// .debounceTime(const Duration(milliseconds: 300))
// .asyncExpand(mapper);
// };
// }
// Initial load
Future<void> _onGetProduct(
_GetProduct event,
Emitter<ProductLoaderState> emit,
) async {
emit(const _Loading());
_isLoadingMore = false; // Reset loading state
final result = await _productRemoteDatasource.getProducts(
page: 1,
limit: 10,
);
await result.fold(
(failure) async => emit(_Error(failure)),
(response) async {
final products = response.data?.products ?? [];
final hasReachedMax = products.length < 10;
emit(_Loaded(
products: products,
hasReachedMax: hasReachedMax,
currentPage: 1,
isLoadingMore: false,
));
},
);
}
// Load more with enhanced debouncing
Future<void> _onLoadMore(
_LoadMore event,
Emitter<ProductLoaderState> emit,
) async {
final currentState = state;
// Enhanced validation
if (currentState is! _Loaded ||
currentState.hasReachedMax ||
_isLoadingMore ||
currentState.isLoadingMore) {
return;
}
_isLoadingMore = true;
// Emit loading more state
emit(currentState.copyWith(isLoadingMore: true));
final nextPage = currentState.currentPage + 1;
try {
final result = await _productRemoteDatasource.getProducts(
page: nextPage,
limit: 10,
);
});
await result.fold(
(failure) async {
// On error, revert loading state but don't show error
// Just silently fail and allow retry
emit(currentState.copyWith(isLoadingMore: false));
_isLoadingMore = false;
},
(response) async {
final newProducts = response.data?.products ?? [];
// Prevent duplicate products
final currentProductIds =
currentState.products.map((p) => p.id).toSet();
final filteredNewProducts = newProducts
.where((product) => !currentProductIds.contains(product.id))
.toList();
final allProducts = List<Product>.from(currentState.products)
..addAll(filteredNewProducts);
final hasReachedMax = newProducts.length < 10;
emit(_Loaded(
products: allProducts,
hasReachedMax: hasReachedMax,
currentPage: nextPage,
isLoadingMore: false,
));
_isLoadingMore = false;
},
);
} catch (e) {
// Handle unexpected errors
emit(currentState.copyWith(isLoadingMore: false));
_isLoadingMore = false;
}
}
// Refresh data
Future<void> _onRefresh(
_Refresh event,
Emitter<ProductLoaderState> emit,
) async {
_isLoadingMore = false;
_loadMoreDebounce?.cancel();
add(const _GetProduct());
}
}

View File

@ -19,32 +19,44 @@ mixin _$ProductLoaderEvent {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() getProduct,
required TResult Function() loadMore,
required TResult Function() refresh,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? getProduct,
TResult? Function()? loadMore,
TResult? Function()? refresh,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? getProduct,
TResult Function()? loadMore,
TResult Function()? refresh,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_GetProduct value) getProduct,
required TResult Function(_LoadMore value) loadMore,
required TResult Function(_Refresh value) refresh,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_GetProduct value)? getProduct,
TResult? Function(_LoadMore value)? loadMore,
TResult? Function(_Refresh value)? refresh,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_GetProduct value)? getProduct,
TResult Function(_LoadMore value)? loadMore,
TResult Function(_Refresh value)? refresh,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
@ -113,6 +125,8 @@ class _$GetProductImpl implements _GetProduct {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() getProduct,
required TResult Function() loadMore,
required TResult Function() refresh,
}) {
return getProduct();
}
@ -121,6 +135,8 @@ class _$GetProductImpl implements _GetProduct {
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? getProduct,
TResult? Function()? loadMore,
TResult? Function()? refresh,
}) {
return getProduct?.call();
}
@ -129,6 +145,8 @@ class _$GetProductImpl implements _GetProduct {
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? getProduct,
TResult Function()? loadMore,
TResult Function()? refresh,
required TResult orElse(),
}) {
if (getProduct != null) {
@ -141,6 +159,8 @@ class _$GetProductImpl implements _GetProduct {
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_GetProduct value) getProduct,
required TResult Function(_LoadMore value) loadMore,
required TResult Function(_Refresh value) refresh,
}) {
return getProduct(this);
}
@ -149,6 +169,8 @@ class _$GetProductImpl implements _GetProduct {
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_GetProduct value)? getProduct,
TResult? Function(_LoadMore value)? loadMore,
TResult? Function(_Refresh value)? refresh,
}) {
return getProduct?.call(this);
}
@ -157,6 +179,8 @@ class _$GetProductImpl implements _GetProduct {
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_GetProduct value)? getProduct,
TResult Function(_LoadMore value)? loadMore,
TResult Function(_Refresh value)? refresh,
required TResult orElse(),
}) {
if (getProduct != null) {
@ -170,13 +194,237 @@ abstract class _GetProduct implements ProductLoaderEvent {
const factory _GetProduct() = _$GetProductImpl;
}
/// @nodoc
abstract class _$$LoadMoreImplCopyWith<$Res> {
factory _$$LoadMoreImplCopyWith(
_$LoadMoreImpl value, $Res Function(_$LoadMoreImpl) then) =
__$$LoadMoreImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$LoadMoreImplCopyWithImpl<$Res>
extends _$ProductLoaderEventCopyWithImpl<$Res, _$LoadMoreImpl>
implements _$$LoadMoreImplCopyWith<$Res> {
__$$LoadMoreImplCopyWithImpl(
_$LoadMoreImpl _value, $Res Function(_$LoadMoreImpl) _then)
: super(_value, _then);
/// Create a copy of ProductLoaderEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$LoadMoreImpl implements _LoadMore {
const _$LoadMoreImpl();
@override
String toString() {
return 'ProductLoaderEvent.loadMore()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$LoadMoreImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() getProduct,
required TResult Function() loadMore,
required TResult Function() refresh,
}) {
return loadMore();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? getProduct,
TResult? Function()? loadMore,
TResult? Function()? refresh,
}) {
return loadMore?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? getProduct,
TResult Function()? loadMore,
TResult Function()? refresh,
required TResult orElse(),
}) {
if (loadMore != null) {
return loadMore();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_GetProduct value) getProduct,
required TResult Function(_LoadMore value) loadMore,
required TResult Function(_Refresh value) refresh,
}) {
return loadMore(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_GetProduct value)? getProduct,
TResult? Function(_LoadMore value)? loadMore,
TResult? Function(_Refresh value)? refresh,
}) {
return loadMore?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_GetProduct value)? getProduct,
TResult Function(_LoadMore value)? loadMore,
TResult Function(_Refresh value)? refresh,
required TResult orElse(),
}) {
if (loadMore != null) {
return loadMore(this);
}
return orElse();
}
}
abstract class _LoadMore implements ProductLoaderEvent {
const factory _LoadMore() = _$LoadMoreImpl;
}
/// @nodoc
abstract class _$$RefreshImplCopyWith<$Res> {
factory _$$RefreshImplCopyWith(
_$RefreshImpl value, $Res Function(_$RefreshImpl) then) =
__$$RefreshImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$RefreshImplCopyWithImpl<$Res>
extends _$ProductLoaderEventCopyWithImpl<$Res, _$RefreshImpl>
implements _$$RefreshImplCopyWith<$Res> {
__$$RefreshImplCopyWithImpl(
_$RefreshImpl _value, $Res Function(_$RefreshImpl) _then)
: super(_value, _then);
/// Create a copy of ProductLoaderEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$RefreshImpl implements _Refresh {
const _$RefreshImpl();
@override
String toString() {
return 'ProductLoaderEvent.refresh()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$RefreshImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() getProduct,
required TResult Function() loadMore,
required TResult Function() refresh,
}) {
return refresh();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? getProduct,
TResult? Function()? loadMore,
TResult? Function()? refresh,
}) {
return refresh?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? getProduct,
TResult Function()? loadMore,
TResult Function()? refresh,
required TResult orElse(),
}) {
if (refresh != null) {
return refresh();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_GetProduct value) getProduct,
required TResult Function(_LoadMore value) loadMore,
required TResult Function(_Refresh value) refresh,
}) {
return refresh(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_GetProduct value)? getProduct,
TResult? Function(_LoadMore value)? loadMore,
TResult? Function(_Refresh value)? refresh,
}) {
return refresh?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_GetProduct value)? getProduct,
TResult Function(_LoadMore value)? loadMore,
TResult Function(_Refresh value)? refresh,
required TResult orElse(),
}) {
if (refresh != null) {
return refresh(this);
}
return orElse();
}
}
abstract class _Refresh implements ProductLoaderEvent {
const factory _Refresh() = _$RefreshImpl;
}
/// @nodoc
mixin _$ProductLoaderState {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(List<Product> products) loaded,
required TResult Function(List<Product> products, bool hasReachedMax,
int currentPage, bool isLoadingMore)
loaded,
required TResult Function(String message) error,
}) =>
throw _privateConstructorUsedError;
@ -184,7 +432,9 @@ mixin _$ProductLoaderState {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(List<Product> products)? loaded,
TResult? Function(List<Product> products, bool hasReachedMax,
int currentPage, bool isLoadingMore)?
loaded,
TResult? Function(String message)? error,
}) =>
throw _privateConstructorUsedError;
@ -192,7 +442,9 @@ mixin _$ProductLoaderState {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(List<Product> products)? loaded,
TResult Function(List<Product> products, bool hasReachedMax,
int currentPage, bool isLoadingMore)?
loaded,
TResult Function(String message)? error,
required TResult orElse(),
}) =>
@ -288,7 +540,9 @@ class _$InitialImpl implements _Initial {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(List<Product> products) loaded,
required TResult Function(List<Product> products, bool hasReachedMax,
int currentPage, bool isLoadingMore)
loaded,
required TResult Function(String message) error,
}) {
return initial();
@ -299,7 +553,9 @@ class _$InitialImpl implements _Initial {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(List<Product> products)? loaded,
TResult? Function(List<Product> products, bool hasReachedMax,
int currentPage, bool isLoadingMore)?
loaded,
TResult? Function(String message)? error,
}) {
return initial?.call();
@ -310,7 +566,9 @@ class _$InitialImpl implements _Initial {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(List<Product> products)? loaded,
TResult Function(List<Product> products, bool hasReachedMax,
int currentPage, bool isLoadingMore)?
loaded,
TResult Function(String message)? error,
required TResult orElse(),
}) {
@ -405,7 +663,9 @@ class _$LoadingImpl implements _Loading {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(List<Product> products) loaded,
required TResult Function(List<Product> products, bool hasReachedMax,
int currentPage, bool isLoadingMore)
loaded,
required TResult Function(String message) error,
}) {
return loading();
@ -416,7 +676,9 @@ class _$LoadingImpl implements _Loading {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(List<Product> products)? loaded,
TResult? Function(List<Product> products, bool hasReachedMax,
int currentPage, bool isLoadingMore)?
loaded,
TResult? Function(String message)? error,
}) {
return loading?.call();
@ -427,7 +689,9 @@ class _$LoadingImpl implements _Loading {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(List<Product> products)? loaded,
TResult Function(List<Product> products, bool hasReachedMax,
int currentPage, bool isLoadingMore)?
loaded,
TResult Function(String message)? error,
required TResult orElse(),
}) {
@ -485,7 +749,11 @@ abstract class _$$LoadedImplCopyWith<$Res> {
_$LoadedImpl value, $Res Function(_$LoadedImpl) then) =
__$$LoadedImplCopyWithImpl<$Res>;
@useResult
$Res call({List<Product> products});
$Res call(
{List<Product> products,
bool hasReachedMax,
int currentPage,
bool isLoadingMore});
}
/// @nodoc
@ -502,12 +770,27 @@ class __$$LoadedImplCopyWithImpl<$Res>
@override
$Res call({
Object? products = null,
Object? hasReachedMax = null,
Object? currentPage = null,
Object? isLoadingMore = null,
}) {
return _then(_$LoadedImpl(
null == products
products: null == products
? _value._products
: products // ignore: cast_nullable_to_non_nullable
as List<Product>,
hasReachedMax: null == hasReachedMax
? _value.hasReachedMax
: hasReachedMax // ignore: cast_nullable_to_non_nullable
as bool,
currentPage: null == currentPage
? _value.currentPage
: currentPage // ignore: cast_nullable_to_non_nullable
as int,
isLoadingMore: null == isLoadingMore
? _value.isLoadingMore
: isLoadingMore // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
@ -515,7 +798,12 @@ class __$$LoadedImplCopyWithImpl<$Res>
/// @nodoc
class _$LoadedImpl implements _Loaded {
const _$LoadedImpl(final List<Product> products) : _products = products;
const _$LoadedImpl(
{required final List<Product> products,
required this.hasReachedMax,
required this.currentPage,
required this.isLoadingMore})
: _products = products;
final List<Product> _products;
@override
@ -525,9 +813,16 @@ class _$LoadedImpl implements _Loaded {
return EqualUnmodifiableListView(_products);
}
@override
final bool hasReachedMax;
@override
final int currentPage;
@override
final bool isLoadingMore;
@override
String toString() {
return 'ProductLoaderState.loaded(products: $products)';
return 'ProductLoaderState.loaded(products: $products, hasReachedMax: $hasReachedMax, currentPage: $currentPage, isLoadingMore: $isLoadingMore)';
}
@override
@ -535,12 +830,22 @@ class _$LoadedImpl implements _Loaded {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LoadedImpl &&
const DeepCollectionEquality().equals(other._products, _products));
const DeepCollectionEquality().equals(other._products, _products) &&
(identical(other.hasReachedMax, hasReachedMax) ||
other.hasReachedMax == hasReachedMax) &&
(identical(other.currentPage, currentPage) ||
other.currentPage == currentPage) &&
(identical(other.isLoadingMore, isLoadingMore) ||
other.isLoadingMore == isLoadingMore));
}
@override
int get hashCode =>
Object.hash(runtimeType, const DeepCollectionEquality().hash(_products));
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_products),
hasReachedMax,
currentPage,
isLoadingMore);
/// Create a copy of ProductLoaderState
/// with the given fields replaced by the non-null parameter values.
@ -555,10 +860,12 @@ class _$LoadedImpl implements _Loaded {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(List<Product> products) loaded,
required TResult Function(List<Product> products, bool hasReachedMax,
int currentPage, bool isLoadingMore)
loaded,
required TResult Function(String message) error,
}) {
return loaded(products);
return loaded(products, hasReachedMax, currentPage, isLoadingMore);
}
@override
@ -566,10 +873,12 @@ class _$LoadedImpl implements _Loaded {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(List<Product> products)? loaded,
TResult? Function(List<Product> products, bool hasReachedMax,
int currentPage, bool isLoadingMore)?
loaded,
TResult? Function(String message)? error,
}) {
return loaded?.call(products);
return loaded?.call(products, hasReachedMax, currentPage, isLoadingMore);
}
@override
@ -577,12 +886,14 @@ class _$LoadedImpl implements _Loaded {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(List<Product> products)? loaded,
TResult Function(List<Product> products, bool hasReachedMax,
int currentPage, bool isLoadingMore)?
loaded,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (loaded != null) {
return loaded(products);
return loaded(products, hasReachedMax, currentPage, isLoadingMore);
}
return orElse();
}
@ -626,9 +937,16 @@ class _$LoadedImpl implements _Loaded {
}
abstract class _Loaded implements ProductLoaderState {
const factory _Loaded(final List<Product> products) = _$LoadedImpl;
const factory _Loaded(
{required final List<Product> products,
required final bool hasReachedMax,
required final int currentPage,
required final bool isLoadingMore}) = _$LoadedImpl;
List<Product> get products;
bool get hasReachedMax;
int get currentPage;
bool get isLoadingMore;
/// Create a copy of ProductLoaderState
/// with the given fields replaced by the non-null parameter values.
@ -707,7 +1025,9 @@ class _$ErrorImpl implements _Error {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(List<Product> products) loaded,
required TResult Function(List<Product> products, bool hasReachedMax,
int currentPage, bool isLoadingMore)
loaded,
required TResult Function(String message) error,
}) {
return error(message);
@ -718,7 +1038,9 @@ class _$ErrorImpl implements _Error {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(List<Product> products)? loaded,
TResult? Function(List<Product> products, bool hasReachedMax,
int currentPage, bool isLoadingMore)?
loaded,
TResult? Function(String message)? error,
}) {
return error?.call(message);
@ -729,7 +1051,9 @@ class _$ErrorImpl implements _Error {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(List<Product> products)? loaded,
TResult Function(List<Product> products, bool hasReachedMax,
int currentPage, bool isLoadingMore)?
loaded,
TResult Function(String message)? error,
required TResult orElse(),
}) {

View File

@ -3,4 +3,6 @@ part of 'product_loader_bloc.dart';
@freezed
class ProductLoaderEvent with _$ProductLoaderEvent {
const factory ProductLoaderEvent.getProduct() = _GetProduct;
const factory ProductLoaderEvent.loadMore() = _LoadMore;
const factory ProductLoaderEvent.refresh() = _Refresh;
}

View File

@ -4,6 +4,11 @@ part of 'product_loader_bloc.dart';
class ProductLoaderState with _$ProductLoaderState {
const factory ProductLoaderState.initial() = _Initial;
const factory ProductLoaderState.loading() = _Loading;
const factory ProductLoaderState.loaded(List<Product> products) = _Loaded;
const factory ProductLoaderState.loaded({
required List<Product> products,
required bool hasReachedMax,
required int currentPage,
required bool isLoadingMore,
}) = _Loaded;
const factory ProductLoaderState.error(String message) = _Error;
}

View File

@ -38,6 +38,7 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> {
final searchController = TextEditingController();
final ScrollController scrollController = ScrollController();
String searchQuery = '';
test() async {
@ -140,171 +141,204 @@ class _HomePageState extends State<HomePage> {
),
BlocBuilder<ProductLoaderBloc, ProductLoaderState>(
builder: (context, state) {
return Expanded(
child: CustomTabBarV2(
tabTitles: const [
'Semua',
'Makanan',
'Minuman',
'Paket'
],
tabViews: [
// All Products Tab
SizedBox(
child: state.maybeWhen(orElse: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loaded: (products) {
final filteredProducts =
_filterProducts(products);
if (filteredProducts.isEmpty) {
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
return state.maybeWhen(
orElse: () => false,
loaded: (products, hasReachedMax, currentPage,
isLoadingMore) {
if (notification is ScrollEndNotification &&
scrollController.position.extentAfter ==
0) {
context.read<ProductLoaderBloc>().add(
const ProductLoaderEvent.loadMore());
return true;
}
return true;
},
);
},
child: Expanded(
child: CustomTabBarV2(
tabTitles: const [
'Semua',
'Makanan',
'Minuman',
'Paket'
],
tabViews: [
// All Products Tab
SizedBox(
child: state.maybeWhen(orElse: () {
return const Center(
child: Text('No Items Found'),
child: CircularProgressIndicator(),
);
}
return GridView.builder(
itemCount: filteredProducts.length,
padding: const EdgeInsets.all(16),
gridDelegate:
SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 180,
mainAxisSpacing: 30,
crossAxisSpacing: 30,
childAspectRatio: 180 / 240,
),
itemBuilder: (context, index) =>
ProductCard(
data: filteredProducts[index],
onCartButton: () {},
),
);
}),
),
// Makanan Tab
SizedBox(
child: state.maybeWhen(orElse: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loaded: (products) {
if (products.isEmpty) {
}, loading: () {
return const Center(
child: Text('No Items'),
child: CircularProgressIndicator(),
);
}
final filteredProducts =
_filterProductsByCategory(products, 1);
return filteredProducts.isEmpty
? const _IsEmpty()
: GridView.builder(
itemCount: filteredProducts.length,
padding: const EdgeInsets.all(16),
gridDelegate:
SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent:
200, // Lebar maksimal tiap item (bisa kamu ubah)
mainAxisSpacing: 30,
crossAxisSpacing: 30,
childAspectRatio: 0.85,
),
itemBuilder: (context, index) =>
ProductCard(
data: filteredProducts[index],
onCartButton: () {},
),
);
}),
),
// Minuman Tab
SizedBox(
child: state.maybeWhen(orElse: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loaded: (products) {
if (products.isEmpty) {
}, loaded: (products, hashasReachedMax,
currentPage, isLoadingMore) {
final filteredProducts =
_filterProducts(products);
if (filteredProducts.isEmpty) {
return const Center(
child: Text('No Items Found'),
);
}
return GridView.builder(
itemCount: filteredProducts.length,
controller: scrollController,
padding: const EdgeInsets.all(16),
gridDelegate:
SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 180,
mainAxisSpacing: 30,
crossAxisSpacing: 30,
childAspectRatio: 180 / 240,
),
itemBuilder: (context, index) =>
ProductCard(
data: filteredProducts[index],
onCartButton: () {},
),
);
}),
),
// Makanan Tab
SizedBox(
child: state.maybeWhen(orElse: () {
return const Center(
child: Text('No Items'),
child: CircularProgressIndicator(),
);
}
final filteredProducts =
_filterProductsByCategory(products, 2);
return filteredProducts.isEmpty
? const _IsEmpty()
: GridView.builder(
itemCount: filteredProducts.length,
padding: const EdgeInsets.all(16),
gridDelegate:
SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent:
200, // Lebar maksimal tiap item (bisa kamu ubah)
mainAxisSpacing: 30,
crossAxisSpacing: 30,
childAspectRatio: 0.85,
),
itemBuilder: (context, index) {
return ProductCard(
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loaded: (products, hashasReachedMax,
currentPage, isLoadingMore) {
if (products.isEmpty) {
return const Center(
child: Text('No Items'),
);
}
final filteredProducts =
_filterProductsByCategory(
products, 1);
return filteredProducts.isEmpty
? const _IsEmpty()
: GridView.builder(
itemCount:
filteredProducts.length,
padding: const EdgeInsets.all(16),
controller: scrollController,
gridDelegate:
SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent:
200, // Lebar maksimal tiap item (bisa kamu ubah)
mainAxisSpacing: 30,
crossAxisSpacing: 30,
childAspectRatio: 0.85,
),
itemBuilder: (context, index) =>
ProductCard(
data: filteredProducts[index],
onCartButton: () {},
);
},
);
}),
),
// Snack Tab
SizedBox(
child: state.maybeWhen(orElse: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loaded: (products) {
if (products.isEmpty) {
),
);
}),
),
// Minuman Tab
SizedBox(
child: state.maybeWhen(orElse: () {
return const Center(
child: Text('No Items'),
child: CircularProgressIndicator(),
);
}
final filteredProducts =
_filterProductsByCategory(products, 3);
return filteredProducts.isEmpty
? const _IsEmpty()
: GridView.builder(
itemCount: filteredProducts.length,
padding: const EdgeInsets.all(16),
gridDelegate:
SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent:
200, // Lebar maksimal tiap item (bisa kamu ubah)
mainAxisSpacing: 30,
crossAxisSpacing: 30,
childAspectRatio: 0.85,
),
itemBuilder: (context, index) {
return ProductCard(
data: filteredProducts[index],
onCartButton: () {},
);
},
);
}),
),
],
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loaded: (products, hashasReachedMax,
currentPage, isLoadingMore) {
if (products.isEmpty) {
return const Center(
child: Text('No Items'),
);
}
final filteredProducts =
_filterProductsByCategory(
products, 2);
return filteredProducts.isEmpty
? const _IsEmpty()
: GridView.builder(
itemCount:
filteredProducts.length,
padding: const EdgeInsets.all(16),
controller: scrollController,
gridDelegate:
SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent:
200, // Lebar maksimal tiap item (bisa kamu ubah)
mainAxisSpacing: 30,
crossAxisSpacing: 30,
childAspectRatio: 0.85,
),
itemBuilder: (context, index) {
return ProductCard(
data: filteredProducts[index],
onCartButton: () {},
);
},
);
}),
),
// Snack Tab
SizedBox(
child: state.maybeWhen(orElse: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loaded: (products, hashasReachedMax,
currentPage, isLoadingMore) {
if (products.isEmpty) {
return const Center(
child: Text('No Items'),
);
}
final filteredProducts =
_filterProductsByCategory(
products, 3);
return filteredProducts.isEmpty
? const _IsEmpty()
: GridView.builder(
itemCount:
filteredProducts.length,
padding: const EdgeInsets.all(16),
controller: scrollController,
gridDelegate:
SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent:
200, // Lebar maksimal tiap item (bisa kamu ubah)
mainAxisSpacing: 30,
crossAxisSpacing: 30,
childAspectRatio: 0.85,
),
itemBuilder: (context, index) {
return ProductCard(
data: filteredProducts[index],
onCartButton: () {},
);
},
);
}),
),
],
),
),
);
},