import 'package:flutter/material.dart'; import 'dart:async'; import '../utils/performance_optimizer.dart'; import '../widgets/skeleton_loading.dart'; /// 레이지 로딩이 적용된 리스트 위젯 class LazyLoadingList extends StatefulWidget { final Future> Function(int page, int pageSize) loadMore; final Widget Function(BuildContext, T, int) itemBuilder; final int pageSize; final double scrollThreshold; final Widget? loadingWidget; final Widget? emptyWidget; final Widget? errorWidget; final bool enableRefresh; final ScrollPhysics? physics; final EdgeInsetsGeometry? padding; const LazyLoadingList({ super.key, required this.loadMore, required this.itemBuilder, this.pageSize = 20, this.scrollThreshold = 0.8, this.loadingWidget, this.emptyWidget, this.errorWidget, this.enableRefresh = true, this.physics, this.padding, }); @override State> createState() => _LazyLoadingListState(); } class _LazyLoadingListState extends State> { final List _items = []; final ScrollController _scrollController = ScrollController(); int _currentPage = 0; bool _isLoading = false; bool _hasMore = true; String? _error; @override void initState() { super.initState(); _loadInitialData(); _scrollController.addListener(_onScroll); } @override void dispose() { _scrollController.dispose(); super.dispose(); } void _onScroll() { if (_isLoading || !_hasMore) return; final position = _scrollController.position; final maxScroll = position.maxScrollExtent; final currentScroll = position.pixels; if (currentScroll >= maxScroll * widget.scrollThreshold) { _loadMoreData(); } } Future _loadInitialData() async { setState(() { _isLoading = true; _error = null; }); try { final newItems = await PerformanceMeasure.measure( name: 'Initial data load', operation: () => widget.loadMore(0, widget.pageSize), ); setState(() { _items.clear(); _items.addAll(newItems); _currentPage = 0; _hasMore = newItems.length >= widget.pageSize; _isLoading = false; }); } catch (e) { setState(() { _error = e.toString(); _isLoading = false; }); } } Future _loadMoreData() async { if (_isLoading || !_hasMore) return; setState(() { _isLoading = true; }); try { final nextPage = _currentPage + 1; final newItems = await PerformanceMeasure.measure( name: 'Load more data (page $nextPage)', operation: () => widget.loadMore(nextPage, widget.pageSize), ); setState(() { _items.addAll(newItems); _currentPage = nextPage; _hasMore = newItems.length >= widget.pageSize; _isLoading = false; }); } catch (e) { setState(() { _error = e.toString(); _isLoading = false; }); } } Future _refresh() async { await _loadInitialData(); } @override Widget build(BuildContext context) { if (_error != null && _items.isEmpty) { return Center( child: widget.errorWidget ?? _buildDefaultErrorWidget(), ); } if (!_isLoading && _items.isEmpty) { return Center( child: widget.emptyWidget ?? _buildDefaultEmptyWidget(), ); } Widget listView = ListView.builder( controller: _scrollController, physics: widget.physics ?? PerformanceOptimizer.getOptimizedScrollPhysics(), padding: widget.padding, itemCount: _items.length + (_isLoading || _hasMore ? 1 : 0), itemBuilder: (context, index) { if (index < _items.length) { return widget.itemBuilder(context, _items[index], index); } // 로딩 인디케이터 return Padding( padding: const EdgeInsets.all(16.0), child: Center( child: widget.loadingWidget ?? _buildDefaultLoadingWidget(), ), ); }, ); if (widget.enableRefresh) { return RefreshIndicator( onRefresh: _refresh, child: listView, ); } return listView; } Widget _buildDefaultLoadingWidget() { return const CircularProgressIndicator(); } Widget _buildDefaultEmptyWidget() { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.inbox_outlined, size: 64, color: Colors.grey[400], ), const SizedBox(height: 16), Text( '데이터가 없습니다', style: TextStyle( fontSize: 16, color: Colors.grey[600], ), ), ], ); } Widget _buildDefaultErrorWidget() { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.error_outline, size: 64, color: Colors.red[400], ), const SizedBox(height: 16), Text( '오류가 발생했습니다', style: TextStyle( fontSize: 16, color: Colors.grey[600], ), ), const SizedBox(height: 8), Text( _error ?? '', style: TextStyle( fontSize: 14, color: Colors.grey[500], ), textAlign: TextAlign.center, ), const SizedBox(height: 16), ElevatedButton( onPressed: _loadInitialData, child: const Text('다시 시도'), ), ], ); } } /// 캐시가 적용된 레이지 로딩 리스트 class CachedLazyLoadingList extends StatefulWidget { final String cacheKey; final Future> Function(int page, int pageSize) loadMore; final Widget Function(BuildContext, T, int) itemBuilder; final int pageSize; final Duration cacheDuration; final Widget? loadingWidget; final Widget? emptyWidget; const CachedLazyLoadingList({ super.key, required this.cacheKey, required this.loadMore, required this.itemBuilder, this.pageSize = 20, this.cacheDuration = const Duration(minutes: 5), this.loadingWidget, this.emptyWidget, }); @override State> createState() => _CachedLazyLoadingListState(); } class _CachedLazyLoadingListState extends State> { final Map> _pageCache = {}; Future> _loadWithCache(int page, int pageSize) async { // 캐시 확인 if (_pageCache.containsKey(page)) { return _pageCache[page]!; } // 데이터 로드 final items = await widget.loadMore(page, pageSize); // 캐시 저장 _pageCache[page] = items; // 일정 시간 후 캐시 제거 Timer(widget.cacheDuration, () { if (mounted) { setState(() { _pageCache.remove(page); }); } }); return items; } @override Widget build(BuildContext context) { return LazyLoadingList( loadMore: _loadWithCache, itemBuilder: widget.itemBuilder, pageSize: widget.pageSize, loadingWidget: widget.loadingWidget, emptyWidget: widget.emptyWidget, ); } } /// 무한 스크롤 그리드 뷰 class LazyLoadingGrid extends StatefulWidget { final Future> Function(int page, int pageSize) loadMore; final Widget Function(BuildContext, T, int) itemBuilder; final int crossAxisCount; final int pageSize; final double scrollThreshold; final double childAspectRatio; final double crossAxisSpacing; final double mainAxisSpacing; final EdgeInsetsGeometry? padding; const LazyLoadingGrid({ super.key, required this.loadMore, required this.itemBuilder, required this.crossAxisCount, this.pageSize = 20, this.scrollThreshold = 0.8, this.childAspectRatio = 1.0, this.crossAxisSpacing = 8.0, this.mainAxisSpacing = 8.0, this.padding, }); @override State> createState() => _LazyLoadingGridState(); } class _LazyLoadingGridState extends State> { final List _items = []; final ScrollController _scrollController = ScrollController(); int _currentPage = 0; bool _isLoading = false; bool _hasMore = true; @override void initState() { super.initState(); _loadInitialData(); _scrollController.addListener(_onScroll); } @override void dispose() { _scrollController.dispose(); super.dispose(); } void _onScroll() { if (_isLoading || !_hasMore) return; final position = _scrollController.position; final maxScroll = position.maxScrollExtent; final currentScroll = position.pixels; if (currentScroll >= maxScroll * widget.scrollThreshold) { _loadMoreData(); } } Future _loadInitialData() async { setState(() => _isLoading = true); final newItems = await widget.loadMore(0, widget.pageSize); setState(() { _items.clear(); _items.addAll(newItems); _currentPage = 0; _hasMore = newItems.length >= widget.pageSize; _isLoading = false; }); } Future _loadMoreData() async { if (_isLoading || !_hasMore) return; setState(() => _isLoading = true); final nextPage = _currentPage + 1; final newItems = await widget.loadMore(nextPage, widget.pageSize); setState(() { _items.addAll(newItems); _currentPage = nextPage; _hasMore = newItems.length >= widget.pageSize; _isLoading = false; }); } @override Widget build(BuildContext context) { return GridView.builder( controller: _scrollController, padding: widget.padding, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: widget.crossAxisCount, childAspectRatio: widget.childAspectRatio, crossAxisSpacing: widget.crossAxisSpacing, mainAxisSpacing: widget.mainAxisSpacing, ), itemCount: _items.length + (_isLoading ? widget.crossAxisCount : 0), itemBuilder: (context, index) { if (index < _items.length) { return widget.itemBuilder(context, _items[index], index); } // 로딩 스켈레톤 return const SkeletonLoading( height: 100, borderRadius: 12, ); }, ); } }