diff --git a/lib/presentation/setting/bloc/get_products/get_products_bloc.dart b/lib/presentation/setting/bloc/get_products/get_products_bloc.dart index c7e05f8..485c792 100644 --- a/lib/presentation/setting/bloc/get_products/get_products_bloc.dart +++ b/lib/presentation/setting/bloc/get_products/get_products_bloc.dart @@ -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'; @@ -8,19 +10,137 @@ part 'get_products_state.dart'; part 'get_products_bloc.freezed.dart'; class GetProductsBloc extends Bloc { - final ProductRemoteDatasource datasource; - GetProductsBloc( - this.datasource, - ) : super(const _Initial()) { - on<_Fetch>((event, emit) async { - emit(const _Loading()); - final response = await datasource.getProducts(); - response.fold( - (l) => emit(_Error(l)), - (r) { - emit(_Success(r.data!.products!)); + final ProductRemoteDatasource _productRemoteDatasource; + + // Debouncing untuk mencegah multiple load more calls + Timer? _loadMoreDebounce; + bool _isLoadingMore = false; + + GetProductsBloc(this._productRemoteDatasource) + : super(GetProductsState.initial()) { + on<_Fetch>(_onGetProduct); + on<_LoadMore>(_onLoadMore); + on<_Refresh>(_onRefresh); + } + + @override + Future close() { + _loadMoreDebounce?.cancel(); + return super.close(); + } + + // Debounce transformer untuk load more + // EventTransformer _debounceTransformer() { + // return (events, mapper) { + // return events + // .debounceTime(const Duration(milliseconds: 300)) + // .asyncExpand(mapper); + // }; + // } + + // Initial load + Future _onGetProduct( + _Fetch event, + Emitter 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(_Success( + products: products, + hasReachedMax: hasReachedMax, + currentPage: 1, + isLoadingMore: false, + )); + }, + ); + } + + // Load more with enhanced debouncing + Future _onLoadMore( + _LoadMore event, + Emitter emit, + ) async { + final currentState = state; + + // Enhanced validation + if (currentState is! _Success || + 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.from(currentState.products) + ..addAll(filteredNewProducts); + + final hasReachedMax = newProducts.length < 10; + + emit(_Success( + 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 _onRefresh( + _Refresh event, + Emitter emit, + ) async { + _isLoadingMore = false; + _loadMoreDebounce?.cancel(); + add(const _Fetch()); } } diff --git a/lib/presentation/setting/bloc/get_products/get_products_bloc.freezed.dart b/lib/presentation/setting/bloc/get_products/get_products_bloc.freezed.dart index 8872dee..41ff493 100644 --- a/lib/presentation/setting/bloc/get_products/get_products_bloc.freezed.dart +++ b/lib/presentation/setting/bloc/get_products/get_products_bloc.freezed.dart @@ -18,39 +18,45 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$GetProductsEvent { @optionalTypeArgs TResult when({ - required TResult Function() started, required TResult Function() fetch, + required TResult Function() loadMore, + required TResult Function() refresh, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ - TResult? Function()? started, TResult? Function()? fetch, + TResult? Function()? loadMore, + TResult? Function()? refresh, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ - TResult Function()? started, TResult Function()? fetch, + TResult Function()? loadMore, + TResult Function()? refresh, required TResult orElse(), }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult map({ - required TResult Function(_Started value) started, required TResult Function(_Fetch value) fetch, + required TResult Function(_LoadMore value) loadMore, + required TResult Function(_Refresh value) refresh, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? mapOrNull({ - TResult? Function(_Started value)? started, TResult? Function(_Fetch value)? fetch, + TResult? Function(_LoadMore value)? loadMore, + TResult? Function(_Refresh value)? refresh, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeMap({ - TResult Function(_Started value)? started, TResult Function(_Fetch value)? fetch, + TResult Function(_LoadMore value)? loadMore, + TResult Function(_Refresh value)? refresh, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -77,111 +83,6 @@ class _$GetProductsEventCopyWithImpl<$Res, $Val extends GetProductsEvent> /// with the given fields replaced by the non-null parameter values. } -/// @nodoc -abstract class _$$StartedImplCopyWith<$Res> { - factory _$$StartedImplCopyWith( - _$StartedImpl value, $Res Function(_$StartedImpl) then) = - __$$StartedImplCopyWithImpl<$Res>; -} - -/// @nodoc -class __$$StartedImplCopyWithImpl<$Res> - extends _$GetProductsEventCopyWithImpl<$Res, _$StartedImpl> - implements _$$StartedImplCopyWith<$Res> { - __$$StartedImplCopyWithImpl( - _$StartedImpl _value, $Res Function(_$StartedImpl) _then) - : super(_value, _then); - - /// Create a copy of GetProductsEvent - /// with the given fields replaced by the non-null parameter values. -} - -/// @nodoc - -class _$StartedImpl implements _Started { - const _$StartedImpl(); - - @override - String toString() { - return 'GetProductsEvent.started()'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && other is _$StartedImpl); - } - - @override - int get hashCode => runtimeType.hashCode; - - @override - @optionalTypeArgs - TResult when({ - required TResult Function() started, - required TResult Function() fetch, - }) { - return started(); - } - - @override - @optionalTypeArgs - TResult? whenOrNull({ - TResult? Function()? started, - TResult? Function()? fetch, - }) { - return started?.call(); - } - - @override - @optionalTypeArgs - TResult maybeWhen({ - TResult Function()? started, - TResult Function()? fetch, - required TResult orElse(), - }) { - if (started != null) { - return started(); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map({ - required TResult Function(_Started value) started, - required TResult Function(_Fetch value) fetch, - }) { - return started(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull({ - TResult? Function(_Started value)? started, - TResult? Function(_Fetch value)? fetch, - }) { - return started?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap({ - TResult Function(_Started value)? started, - TResult Function(_Fetch value)? fetch, - required TResult orElse(), - }) { - if (started != null) { - return started(this); - } - return orElse(); - } -} - -abstract class _Started implements GetProductsEvent { - const factory _Started() = _$StartedImpl; -} - /// @nodoc abstract class _$$FetchImplCopyWith<$Res> { factory _$$FetchImplCopyWith( @@ -223,8 +124,9 @@ class _$FetchImpl implements _Fetch { @override @optionalTypeArgs TResult when({ - required TResult Function() started, required TResult Function() fetch, + required TResult Function() loadMore, + required TResult Function() refresh, }) { return fetch(); } @@ -232,8 +134,9 @@ class _$FetchImpl implements _Fetch { @override @optionalTypeArgs TResult? whenOrNull({ - TResult? Function()? started, TResult? Function()? fetch, + TResult? Function()? loadMore, + TResult? Function()? refresh, }) { return fetch?.call(); } @@ -241,8 +144,9 @@ class _$FetchImpl implements _Fetch { @override @optionalTypeArgs TResult maybeWhen({ - TResult Function()? started, TResult Function()? fetch, + TResult Function()? loadMore, + TResult Function()? refresh, required TResult orElse(), }) { if (fetch != null) { @@ -254,8 +158,9 @@ class _$FetchImpl implements _Fetch { @override @optionalTypeArgs TResult map({ - required TResult Function(_Started value) started, required TResult Function(_Fetch value) fetch, + required TResult Function(_LoadMore value) loadMore, + required TResult Function(_Refresh value) refresh, }) { return fetch(this); } @@ -263,8 +168,9 @@ class _$FetchImpl implements _Fetch { @override @optionalTypeArgs TResult? mapOrNull({ - TResult? Function(_Started value)? started, TResult? Function(_Fetch value)? fetch, + TResult? Function(_LoadMore value)? loadMore, + TResult? Function(_Refresh value)? refresh, }) { return fetch?.call(this); } @@ -272,8 +178,9 @@ class _$FetchImpl implements _Fetch { @override @optionalTypeArgs TResult maybeMap({ - TResult Function(_Started value)? started, TResult Function(_Fetch value)? fetch, + TResult Function(_LoadMore value)? loadMore, + TResult Function(_Refresh value)? refresh, required TResult orElse(), }) { if (fetch != null) { @@ -287,13 +194,237 @@ abstract class _Fetch implements GetProductsEvent { const factory _Fetch() = _$FetchImpl; } +/// @nodoc +abstract class _$$LoadMoreImplCopyWith<$Res> { + factory _$$LoadMoreImplCopyWith( + _$LoadMoreImpl value, $Res Function(_$LoadMoreImpl) then) = + __$$LoadMoreImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LoadMoreImplCopyWithImpl<$Res> + extends _$GetProductsEventCopyWithImpl<$Res, _$LoadMoreImpl> + implements _$$LoadMoreImplCopyWith<$Res> { + __$$LoadMoreImplCopyWithImpl( + _$LoadMoreImpl _value, $Res Function(_$LoadMoreImpl) _then) + : super(_value, _then); + + /// Create a copy of GetProductsEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$LoadMoreImpl implements _LoadMore { + const _$LoadMoreImpl(); + + @override + String toString() { + return 'GetProductsEvent.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({ + required TResult Function() fetch, + required TResult Function() loadMore, + required TResult Function() refresh, + }) { + return loadMore(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? fetch, + TResult? Function()? loadMore, + TResult? Function()? refresh, + }) { + return loadMore?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? fetch, + TResult Function()? loadMore, + TResult Function()? refresh, + required TResult orElse(), + }) { + if (loadMore != null) { + return loadMore(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Fetch value) fetch, + required TResult Function(_LoadMore value) loadMore, + required TResult Function(_Refresh value) refresh, + }) { + return loadMore(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Fetch value)? fetch, + TResult? Function(_LoadMore value)? loadMore, + TResult? Function(_Refresh value)? refresh, + }) { + return loadMore?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Fetch value)? fetch, + 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 GetProductsEvent { + const factory _LoadMore() = _$LoadMoreImpl; +} + +/// @nodoc +abstract class _$$RefreshImplCopyWith<$Res> { + factory _$$RefreshImplCopyWith( + _$RefreshImpl value, $Res Function(_$RefreshImpl) then) = + __$$RefreshImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$RefreshImplCopyWithImpl<$Res> + extends _$GetProductsEventCopyWithImpl<$Res, _$RefreshImpl> + implements _$$RefreshImplCopyWith<$Res> { + __$$RefreshImplCopyWithImpl( + _$RefreshImpl _value, $Res Function(_$RefreshImpl) _then) + : super(_value, _then); + + /// Create a copy of GetProductsEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$RefreshImpl implements _Refresh { + const _$RefreshImpl(); + + @override + String toString() { + return 'GetProductsEvent.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({ + required TResult Function() fetch, + required TResult Function() loadMore, + required TResult Function() refresh, + }) { + return refresh(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? fetch, + TResult? Function()? loadMore, + TResult? Function()? refresh, + }) { + return refresh?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? fetch, + TResult Function()? loadMore, + TResult Function()? refresh, + required TResult orElse(), + }) { + if (refresh != null) { + return refresh(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Fetch value) fetch, + required TResult Function(_LoadMore value) loadMore, + required TResult Function(_Refresh value) refresh, + }) { + return refresh(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Fetch value)? fetch, + TResult? Function(_LoadMore value)? loadMore, + TResult? Function(_Refresh value)? refresh, + }) { + return refresh?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Fetch value)? fetch, + 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 GetProductsEvent { + const factory _Refresh() = _$RefreshImpl; +} + /// @nodoc mixin _$GetProductsState { @optionalTypeArgs TResult when({ required TResult Function() initial, required TResult Function() loading, - required TResult Function(List products) success, + required TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore) + success, required TResult Function(String message) error, }) => throw _privateConstructorUsedError; @@ -301,7 +432,9 @@ mixin _$GetProductsState { TResult? whenOrNull({ TResult? Function()? initial, TResult? Function()? loading, - TResult? Function(List products)? success, + TResult? Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + success, TResult? Function(String message)? error, }) => throw _privateConstructorUsedError; @@ -309,7 +442,9 @@ mixin _$GetProductsState { TResult maybeWhen({ TResult Function()? initial, TResult Function()? loading, - TResult Function(List products)? success, + TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + success, TResult Function(String message)? error, required TResult orElse(), }) => @@ -405,7 +540,9 @@ class _$InitialImpl implements _Initial { TResult when({ required TResult Function() initial, required TResult Function() loading, - required TResult Function(List products) success, + required TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore) + success, required TResult Function(String message) error, }) { return initial(); @@ -416,7 +553,9 @@ class _$InitialImpl implements _Initial { TResult? whenOrNull({ TResult? Function()? initial, TResult? Function()? loading, - TResult? Function(List products)? success, + TResult? Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + success, TResult? Function(String message)? error, }) { return initial?.call(); @@ -427,7 +566,9 @@ class _$InitialImpl implements _Initial { TResult maybeWhen({ TResult Function()? initial, TResult Function()? loading, - TResult Function(List products)? success, + TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + success, TResult Function(String message)? error, required TResult orElse(), }) { @@ -522,7 +663,9 @@ class _$LoadingImpl implements _Loading { TResult when({ required TResult Function() initial, required TResult Function() loading, - required TResult Function(List products) success, + required TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore) + success, required TResult Function(String message) error, }) { return loading(); @@ -533,7 +676,9 @@ class _$LoadingImpl implements _Loading { TResult? whenOrNull({ TResult? Function()? initial, TResult? Function()? loading, - TResult? Function(List products)? success, + TResult? Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + success, TResult? Function(String message)? error, }) { return loading?.call(); @@ -544,7 +689,9 @@ class _$LoadingImpl implements _Loading { TResult maybeWhen({ TResult Function()? initial, TResult Function()? loading, - TResult Function(List products)? success, + TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + success, TResult Function(String message)? error, required TResult orElse(), }) { @@ -602,7 +749,11 @@ abstract class _$$SuccessImplCopyWith<$Res> { _$SuccessImpl value, $Res Function(_$SuccessImpl) then) = __$$SuccessImplCopyWithImpl<$Res>; @useResult - $Res call({List products}); + $Res call( + {List products, + bool hasReachedMax, + int currentPage, + bool isLoadingMore}); } /// @nodoc @@ -619,12 +770,27 @@ class __$$SuccessImplCopyWithImpl<$Res> @override $Res call({ Object? products = null, + Object? hasReachedMax = null, + Object? currentPage = null, + Object? isLoadingMore = null, }) { return _then(_$SuccessImpl( - null == products + products: null == products ? _value._products : products // ignore: cast_nullable_to_non_nullable as List, + 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, )); } } @@ -632,7 +798,12 @@ class __$$SuccessImplCopyWithImpl<$Res> /// @nodoc class _$SuccessImpl implements _Success { - const _$SuccessImpl(final List products) : _products = products; + const _$SuccessImpl( + {required final List products, + required this.hasReachedMax, + required this.currentPage, + required this.isLoadingMore}) + : _products = products; final List _products; @override @@ -642,9 +813,16 @@ class _$SuccessImpl implements _Success { return EqualUnmodifiableListView(_products); } + @override + final bool hasReachedMax; + @override + final int currentPage; + @override + final bool isLoadingMore; + @override String toString() { - return 'GetProductsState.success(products: $products)'; + return 'GetProductsState.success(products: $products, hasReachedMax: $hasReachedMax, currentPage: $currentPage, isLoadingMore: $isLoadingMore)'; } @override @@ -652,12 +830,22 @@ class _$SuccessImpl implements _Success { return identical(this, other) || (other.runtimeType == runtimeType && other is _$SuccessImpl && - 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 GetProductsState /// with the given fields replaced by the non-null parameter values. @@ -672,10 +860,12 @@ class _$SuccessImpl implements _Success { TResult when({ required TResult Function() initial, required TResult Function() loading, - required TResult Function(List products) success, + required TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore) + success, required TResult Function(String message) error, }) { - return success(products); + return success(products, hasReachedMax, currentPage, isLoadingMore); } @override @@ -683,10 +873,12 @@ class _$SuccessImpl implements _Success { TResult? whenOrNull({ TResult? Function()? initial, TResult? Function()? loading, - TResult? Function(List products)? success, + TResult? Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + success, TResult? Function(String message)? error, }) { - return success?.call(products); + return success?.call(products, hasReachedMax, currentPage, isLoadingMore); } @override @@ -694,12 +886,14 @@ class _$SuccessImpl implements _Success { TResult maybeWhen({ TResult Function()? initial, TResult Function()? loading, - TResult Function(List products)? success, + TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + success, TResult Function(String message)? error, required TResult orElse(), }) { if (success != null) { - return success(products); + return success(products, hasReachedMax, currentPage, isLoadingMore); } return orElse(); } @@ -743,9 +937,16 @@ class _$SuccessImpl implements _Success { } abstract class _Success implements GetProductsState { - const factory _Success(final List products) = _$SuccessImpl; + const factory _Success( + {required final List products, + required final bool hasReachedMax, + required final int currentPage, + required final bool isLoadingMore}) = _$SuccessImpl; List get products; + bool get hasReachedMax; + int get currentPage; + bool get isLoadingMore; /// Create a copy of GetProductsState /// with the given fields replaced by the non-null parameter values. @@ -824,7 +1025,9 @@ class _$ErrorImpl implements _Error { TResult when({ required TResult Function() initial, required TResult Function() loading, - required TResult Function(List products) success, + required TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore) + success, required TResult Function(String message) error, }) { return error(message); @@ -835,7 +1038,9 @@ class _$ErrorImpl implements _Error { TResult? whenOrNull({ TResult? Function()? initial, TResult? Function()? loading, - TResult? Function(List products)? success, + TResult? Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + success, TResult? Function(String message)? error, }) { return error?.call(message); @@ -846,7 +1051,9 @@ class _$ErrorImpl implements _Error { TResult maybeWhen({ TResult Function()? initial, TResult Function()? loading, - TResult Function(List products)? success, + TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + success, TResult Function(String message)? error, required TResult orElse(), }) { diff --git a/lib/presentation/setting/bloc/get_products/get_products_event.dart b/lib/presentation/setting/bloc/get_products/get_products_event.dart index 18fe6d2..0f28bf8 100644 --- a/lib/presentation/setting/bloc/get_products/get_products_event.dart +++ b/lib/presentation/setting/bloc/get_products/get_products_event.dart @@ -2,6 +2,7 @@ part of 'get_products_bloc.dart'; @freezed class GetProductsEvent with _$GetProductsEvent { - const factory GetProductsEvent.started() = _Started; const factory GetProductsEvent.fetch() = _Fetch; + const factory GetProductsEvent.loadMore() = _LoadMore; + const factory GetProductsEvent.refresh() = _Refresh; } diff --git a/lib/presentation/setting/bloc/get_products/get_products_state.dart b/lib/presentation/setting/bloc/get_products/get_products_state.dart index a0b9636..4c786ae 100644 --- a/lib/presentation/setting/bloc/get_products/get_products_state.dart +++ b/lib/presentation/setting/bloc/get_products/get_products_state.dart @@ -4,6 +4,11 @@ part of 'get_products_bloc.dart'; class GetProductsState with _$GetProductsState { const factory GetProductsState.initial() = _Initial; const factory GetProductsState.loading() = _Loading; - const factory GetProductsState.success(List products) = _Success; + const factory GetProductsState.success({ + required List products, + required bool hasReachedMax, + required int currentPage, + required bool isLoadingMore, + }) = _Success; const factory GetProductsState.error(String message) = _Error; } diff --git a/lib/presentation/setting/pages/product_page.dart b/lib/presentation/setting/pages/product_page.dart index eb6d8c3..14d66d0 100644 --- a/lib/presentation/setting/pages/product_page.dart +++ b/lib/presentation/setting/pages/product_page.dart @@ -19,6 +19,7 @@ class ProductPage extends StatefulWidget { } class _ProductPageState extends State { + ScrollController scrollController = ScrollController(); @override void initState() { context.read().add(const GetProductsEvent.fetch()); @@ -64,48 +65,66 @@ class _ProductPageState extends State { Expanded( child: BlocBuilder( builder: (context, state) { - return state.maybeWhen(orElse: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, success: (products) { - return GridView.builder( - padding: EdgeInsets.all(16), - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 200, - mainAxisSpacing: 30, - crossAxisSpacing: 30, - childAspectRatio: 0.85, - ), - itemCount: products.length, - itemBuilder: (BuildContext context, int index) { - final item = products[index]; - return MenuProductItem( - data: item, - onTapEdit: () { - showDialog( - context: context, - builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => UpdateProductBloc( - ProductRemoteDatasource()), - ), - BlocProvider.value( - value: context.read(), - ), - BlocProvider.value( - value: context.read(), - ), - ], - child: FormProductDialog(product: item), - ), - ); - }, - ); - }, - ); - }); + return NotificationListener( + onNotification: (notification) { + return state.maybeWhen( + orElse: () => false, + success: (products, hasReachedMax, currentPage, _) { + if (notification is ScrollEndNotification && + scrollController.position.extentAfter == 0) { + context + .read() + .add(const GetProductsEvent.loadMore()); + return true; + } + + return true; + }); + }, + child: state.maybeWhen(orElse: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, success: (products, hasReachedMax, currentPage, _) { + return GridView.builder( + padding: EdgeInsets.all(16), + controller: scrollController, + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisSpacing: 30, + crossAxisSpacing: 30, + childAspectRatio: 0.85, + ), + itemCount: products.length, + itemBuilder: (BuildContext context, int index) { + final item = products[index]; + return MenuProductItem( + data: item, + onTapEdit: () { + showDialog( + context: context, + builder: (context) => MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => UpdateProductBloc( + ProductRemoteDatasource()), + ), + BlocProvider.value( + value: context.read(), + ), + BlocProvider.value( + value: context.read(), + ), + ], + child: FormProductDialog(product: item), + ), + ); + }, + ); + }, + ); + }), + ); }, ), ),