import 'package:sysfarma_mobile/src/core/network/mobile_api_response.dart'; import 'package:flutter/material.dart'; /// A mixin that adds infinite-scroll pagination helpers to any [State]. /// /// Usage: /// 1. Mix into a [State] subclass. /// 2. Call [initPagination] from [initState] (passing a [ScrollController]). /// 3. Override or assign [onLoadPage] to perform the actual API call. /// 4. Call [resetPagination] when you need to reload from page 1. /// 5. Call [disposePagination] from [dispose]. mixin PaginatedListMixin on State { final List> paginatedItems = []; int _currentPage = 1; bool _hasMore = true; bool _isLoadingMore = false; /// Whether the mixin is currently fetching the next page. bool get isLoadingMore => _isLoadingMore; /// Whether there are more pages available. bool get hasMore => _hasMore; /// The current page number (1-based). int get currentPage => _currentPage; ScrollController? _paginationScrollController; /// The callback that fetches a page of data from the API. /// It receives the page number and must return a [MobileApiResponse] whose /// data is a list of maps. Future>>> Function(int page)? onLoadPage; /// Attach a [ScrollController] and start listening for scroll-near-bottom. void initPagination(ScrollController controller) { _paginationScrollController = controller; controller.addListener(_onScroll); } /// Remove the scroll listener. Call from [dispose]. void disposePagination() { _paginationScrollController?.removeListener(_onScroll); } void _onScroll() { final controller = _paginationScrollController; if (controller == null || !controller.hasClients) { return; } final threshold = controller.position.maxScrollExtent - 200; if (controller.position.pixels >= threshold) { loadNextPage(); } } /// Load the next page and append results to [paginatedItems]. Future loadNextPage() async { if (_isLoadingMore || !_hasMore || onLoadPage == null) { return; } setState(() => _isLoadingMore = true); try { final response = await onLoadPage!(_currentPage + 1); if (!mounted) { return; } if (response.success) { final newItems = response.data ?? const []; final hasMore = response.meta['has_more'] == true; setState(() { paginatedItems.addAll(newItems); _currentPage += 1; _hasMore = hasMore && newItems.isNotEmpty; _isLoadingMore = false; }); } else { setState(() => _isLoadingMore = false); } } catch (_) { if (mounted) { setState(() => _isLoadingMore = false); } } } /// Reset pagination state and reload from page 1. /// /// [firstPageItems] should be the data from the initial API call (page 1). /// [firstPageMeta] is the meta map from that same response. void resetPagination({ required List> firstPageItems, required Map firstPageMeta, }) { paginatedItems.clear(); paginatedItems.addAll(firstPageItems); _currentPage = 1; _hasMore = firstPageMeta['has_more'] == true && firstPageItems.isNotEmpty; _isLoadingMore = false; } /// A convenience widget to place at the bottom of a scrollable list. /// Shows a small spinner when a new page is being fetched. Widget buildLoadMoreIndicator() { if (!_isLoadingMore) { return const SizedBox.shrink(); } return const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Center( child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2), ), ), ); } }