dev #1

Merged
aefril merged 128 commits from dev into main 2025-08-13 17:19:48 +00:00
6 changed files with 692 additions and 194 deletions
Showing only changes of commit 3e9b20f237 - Show all commits

View File

@ -13,17 +13,27 @@ import 'auth_local_datasource.dart';
class ProductRemoteDatasource { class ProductRemoteDatasource {
final Dio dio = DioClient.instance; 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 { try {
final authData = await AuthLocalDataSource().getAuthData(); final authData = await AuthLocalDataSource().getAuthData();
final url = '${Variables.baseUrl}/api/v1/products'; 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( final response = await dio.get(
url, url,
queryParameters: { queryParameters: queryParameters,
'page': 1,
'limit': 30,
},
options: Options( options: Options(
headers: { headers: {
'Authorization': 'Bearer ${authData.token}', 'Authorization': 'Bearer ${authData.token}',

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:enaklo_pos/data/datasources/product_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/product_remote_datasource.dart';
import 'package:enaklo_pos/data/models/response/product_response_model.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> { class ProductLoaderBloc extends Bloc<ProductLoaderEvent, ProductLoaderState> {
final ProductRemoteDatasource _productRemoteDatasource; final ProductRemoteDatasource _productRemoteDatasource;
// Debouncing untuk mencegah multiple load more calls
Timer? _loadMoreDebounce;
bool _isLoadingMore = false;
ProductLoaderBloc(this._productRemoteDatasource) ProductLoaderBloc(this._productRemoteDatasource)
: super(ProductLoaderState.initial()) { : super(ProductLoaderState.initial()) {
on<_GetProduct>((event, emit) async { on<_GetProduct>(_onGetProduct);
emit(const _Loading()); on<_LoadMore>(_onLoadMore);
final result = await _productRemoteDatasource.getProducts(); on<_Refresh>(_onRefresh);
result.fold( }
(l) => emit(_Error(l)),
(r) => emit(_Loaded(r.data?.products ?? [])), @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 @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() getProduct, required TResult Function() getProduct,
required TResult Function() loadMore,
required TResult Function() refresh,
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? getProduct, TResult? Function()? getProduct,
TResult? Function()? loadMore,
TResult? Function()? refresh,
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? getProduct, TResult Function()? getProduct,
TResult Function()? loadMore,
TResult Function()? refresh,
required TResult orElse(), required TResult orElse(),
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@optionalTypeArgs @optionalTypeArgs
TResult map<TResult extends Object?>({ TResult map<TResult extends Object?>({
required TResult Function(_GetProduct value) getProduct, required TResult Function(_GetProduct value) getProduct,
required TResult Function(_LoadMore value) loadMore,
required TResult Function(_Refresh value) refresh,
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@optionalTypeArgs @optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({ TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_GetProduct value)? getProduct, TResult? Function(_GetProduct value)? getProduct,
TResult? Function(_LoadMore value)? loadMore,
TResult? Function(_Refresh value)? refresh,
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@optionalTypeArgs @optionalTypeArgs
TResult maybeMap<TResult extends Object?>({ TResult maybeMap<TResult extends Object?>({
TResult Function(_GetProduct value)? getProduct, TResult Function(_GetProduct value)? getProduct,
TResult Function(_LoadMore value)? loadMore,
TResult Function(_Refresh value)? refresh,
required TResult orElse(), required TResult orElse(),
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@ -113,6 +125,8 @@ class _$GetProductImpl implements _GetProduct {
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() getProduct, required TResult Function() getProduct,
required TResult Function() loadMore,
required TResult Function() refresh,
}) { }) {
return getProduct(); return getProduct();
} }
@ -121,6 +135,8 @@ class _$GetProductImpl implements _GetProduct {
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? getProduct, TResult? Function()? getProduct,
TResult? Function()? loadMore,
TResult? Function()? refresh,
}) { }) {
return getProduct?.call(); return getProduct?.call();
} }
@ -129,6 +145,8 @@ class _$GetProductImpl implements _GetProduct {
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? getProduct, TResult Function()? getProduct,
TResult Function()? loadMore,
TResult Function()? refresh,
required TResult orElse(), required TResult orElse(),
}) { }) {
if (getProduct != null) { if (getProduct != null) {
@ -141,6 +159,8 @@ class _$GetProductImpl implements _GetProduct {
@optionalTypeArgs @optionalTypeArgs
TResult map<TResult extends Object?>({ TResult map<TResult extends Object?>({
required TResult Function(_GetProduct value) getProduct, required TResult Function(_GetProduct value) getProduct,
required TResult Function(_LoadMore value) loadMore,
required TResult Function(_Refresh value) refresh,
}) { }) {
return getProduct(this); return getProduct(this);
} }
@ -149,6 +169,8 @@ class _$GetProductImpl implements _GetProduct {
@optionalTypeArgs @optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({ TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_GetProduct value)? getProduct, TResult? Function(_GetProduct value)? getProduct,
TResult? Function(_LoadMore value)? loadMore,
TResult? Function(_Refresh value)? refresh,
}) { }) {
return getProduct?.call(this); return getProduct?.call(this);
} }
@ -157,6 +179,8 @@ class _$GetProductImpl implements _GetProduct {
@optionalTypeArgs @optionalTypeArgs
TResult maybeMap<TResult extends Object?>({ TResult maybeMap<TResult extends Object?>({
TResult Function(_GetProduct value)? getProduct, TResult Function(_GetProduct value)? getProduct,
TResult Function(_LoadMore value)? loadMore,
TResult Function(_Refresh value)? refresh,
required TResult orElse(), required TResult orElse(),
}) { }) {
if (getProduct != null) { if (getProduct != null) {
@ -170,13 +194,237 @@ abstract class _GetProduct implements ProductLoaderEvent {
const factory _GetProduct() = _$GetProductImpl; 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 /// @nodoc
mixin _$ProductLoaderState { mixin _$ProductLoaderState {
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() initial, required TResult Function() initial,
required TResult Function() loading, 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, required TResult Function(String message) error,
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@ -184,7 +432,9 @@ mixin _$ProductLoaderState {
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial, TResult? Function()? initial,
TResult? Function()? loading, 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, TResult? Function(String message)? error,
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@ -192,7 +442,9 @@ mixin _$ProductLoaderState {
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial, TResult Function()? initial,
TResult Function()? loading, 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, TResult Function(String message)? error,
required TResult orElse(), required TResult orElse(),
}) => }) =>
@ -288,7 +540,9 @@ class _$InitialImpl implements _Initial {
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() initial, required TResult Function() initial,
required TResult Function() loading, 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, required TResult Function(String message) error,
}) { }) {
return initial(); return initial();
@ -299,7 +553,9 @@ class _$InitialImpl implements _Initial {
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial, TResult? Function()? initial,
TResult? Function()? loading, 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, TResult? Function(String message)? error,
}) { }) {
return initial?.call(); return initial?.call();
@ -310,7 +566,9 @@ class _$InitialImpl implements _Initial {
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial, TResult Function()? initial,
TResult Function()? loading, 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, TResult Function(String message)? error,
required TResult orElse(), required TResult orElse(),
}) { }) {
@ -405,7 +663,9 @@ class _$LoadingImpl implements _Loading {
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() initial, required TResult Function() initial,
required TResult Function() loading, 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, required TResult Function(String message) error,
}) { }) {
return loading(); return loading();
@ -416,7 +676,9 @@ class _$LoadingImpl implements _Loading {
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial, TResult? Function()? initial,
TResult? Function()? loading, 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, TResult? Function(String message)? error,
}) { }) {
return loading?.call(); return loading?.call();
@ -427,7 +689,9 @@ class _$LoadingImpl implements _Loading {
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial, TResult Function()? initial,
TResult Function()? loading, 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, TResult Function(String message)? error,
required TResult orElse(), required TResult orElse(),
}) { }) {
@ -485,7 +749,11 @@ abstract class _$$LoadedImplCopyWith<$Res> {
_$LoadedImpl value, $Res Function(_$LoadedImpl) then) = _$LoadedImpl value, $Res Function(_$LoadedImpl) then) =
__$$LoadedImplCopyWithImpl<$Res>; __$$LoadedImplCopyWithImpl<$Res>;
@useResult @useResult
$Res call({List<Product> products}); $Res call(
{List<Product> products,
bool hasReachedMax,
int currentPage,
bool isLoadingMore});
} }
/// @nodoc /// @nodoc
@ -502,12 +770,27 @@ class __$$LoadedImplCopyWithImpl<$Res>
@override @override
$Res call({ $Res call({
Object? products = null, Object? products = null,
Object? hasReachedMax = null,
Object? currentPage = null,
Object? isLoadingMore = null,
}) { }) {
return _then(_$LoadedImpl( return _then(_$LoadedImpl(
null == products products: null == products
? _value._products ? _value._products
: products // ignore: cast_nullable_to_non_nullable : products // ignore: cast_nullable_to_non_nullable
as List<Product>, 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 /// @nodoc
class _$LoadedImpl implements _Loaded { 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; final List<Product> _products;
@override @override
@ -525,9 +813,16 @@ class _$LoadedImpl implements _Loaded {
return EqualUnmodifiableListView(_products); return EqualUnmodifiableListView(_products);
} }
@override
final bool hasReachedMax;
@override
final int currentPage;
@override
final bool isLoadingMore;
@override @override
String toString() { String toString() {
return 'ProductLoaderState.loaded(products: $products)'; return 'ProductLoaderState.loaded(products: $products, hasReachedMax: $hasReachedMax, currentPage: $currentPage, isLoadingMore: $isLoadingMore)';
} }
@override @override
@ -535,12 +830,22 @@ class _$LoadedImpl implements _Loaded {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$LoadedImpl && 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 @override
int get hashCode => int get hashCode => Object.hash(
Object.hash(runtimeType, const DeepCollectionEquality().hash(_products)); runtimeType,
const DeepCollectionEquality().hash(_products),
hasReachedMax,
currentPage,
isLoadingMore);
/// Create a copy of ProductLoaderState /// Create a copy of ProductLoaderState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -555,10 +860,12 @@ class _$LoadedImpl implements _Loaded {
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() initial, required TResult Function() initial,
required TResult Function() loading, 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, required TResult Function(String message) error,
}) { }) {
return loaded(products); return loaded(products, hasReachedMax, currentPage, isLoadingMore);
} }
@override @override
@ -566,10 +873,12 @@ class _$LoadedImpl implements _Loaded {
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial, TResult? Function()? initial,
TResult? Function()? loading, 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, TResult? Function(String message)? error,
}) { }) {
return loaded?.call(products); return loaded?.call(products, hasReachedMax, currentPage, isLoadingMore);
} }
@override @override
@ -577,12 +886,14 @@ class _$LoadedImpl implements _Loaded {
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial, TResult Function()? initial,
TResult Function()? loading, 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, TResult Function(String message)? error,
required TResult orElse(), required TResult orElse(),
}) { }) {
if (loaded != null) { if (loaded != null) {
return loaded(products); return loaded(products, hasReachedMax, currentPage, isLoadingMore);
} }
return orElse(); return orElse();
} }
@ -626,9 +937,16 @@ class _$LoadedImpl implements _Loaded {
} }
abstract class _Loaded implements ProductLoaderState { 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; List<Product> get products;
bool get hasReachedMax;
int get currentPage;
bool get isLoadingMore;
/// Create a copy of ProductLoaderState /// Create a copy of ProductLoaderState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -707,7 +1025,9 @@ class _$ErrorImpl implements _Error {
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() initial, required TResult Function() initial,
required TResult Function() loading, 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, required TResult Function(String message) error,
}) { }) {
return error(message); return error(message);
@ -718,7 +1038,9 @@ class _$ErrorImpl implements _Error {
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial, TResult? Function()? initial,
TResult? Function()? loading, 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, TResult? Function(String message)? error,
}) { }) {
return error?.call(message); return error?.call(message);
@ -729,7 +1051,9 @@ class _$ErrorImpl implements _Error {
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial, TResult Function()? initial,
TResult Function()? loading, 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, TResult Function(String message)? error,
required TResult orElse(), required TResult orElse(),
}) { }) {

View File

@ -3,4 +3,6 @@ part of 'product_loader_bloc.dart';
@freezed @freezed
class ProductLoaderEvent with _$ProductLoaderEvent { class ProductLoaderEvent with _$ProductLoaderEvent {
const factory ProductLoaderEvent.getProduct() = _GetProduct; 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 { class ProductLoaderState with _$ProductLoaderState {
const factory ProductLoaderState.initial() = _Initial; const factory ProductLoaderState.initial() = _Initial;
const factory ProductLoaderState.loading() = _Loading; 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; const factory ProductLoaderState.error(String message) = _Error;
} }

View File

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