Major UI/UX and architecture improvements
- Implemented new navigation system with NavigationProvider and route management - Added adaptive theme system with ThemeProvider for better theme handling - Introduced glassmorphism design elements (app bars, scaffolds, cards) - Added advanced animations (spring animations, page transitions, staggered lists) - Implemented performance optimizations (memory manager, lazy loading) - Refactored Analysis screen into modular components - Added floating navigation bar with haptic feedback - Improved subscription cards with swipe actions - Enhanced skeleton loading with better animations - Added cached network image support - Improved overall app architecture and code organization 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
416
lib/widgets/lazy_loading_list.dart
Normal file
416
lib/widgets/lazy_loading_list.dart
Normal file
@@ -0,0 +1,416 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
import '../utils/performance_optimizer.dart';
|
||||
import '../widgets/skeleton_loading.dart';
|
||||
|
||||
/// 레이지 로딩이 적용된 리스트 위젯
|
||||
class LazyLoadingList<T> extends StatefulWidget {
|
||||
final Future<List<T>> 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<LazyLoadingList<T>> createState() => _LazyLoadingListState<T>();
|
||||
}
|
||||
|
||||
class _LazyLoadingListState<T> extends State<LazyLoadingList<T>> {
|
||||
final List<T> _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<void> _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<void> _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<void> _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<T> extends StatefulWidget {
|
||||
final String cacheKey;
|
||||
final Future<List<T>> 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<CachedLazyLoadingList<T>> createState() => _CachedLazyLoadingListState<T>();
|
||||
}
|
||||
|
||||
class _CachedLazyLoadingListState<T> extends State<CachedLazyLoadingList<T>> {
|
||||
final Map<int, List<T>> _pageCache = {};
|
||||
|
||||
Future<List<T>> _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<T>(
|
||||
loadMore: _loadWithCache,
|
||||
itemBuilder: widget.itemBuilder,
|
||||
pageSize: widget.pageSize,
|
||||
loadingWidget: widget.loadingWidget,
|
||||
emptyWidget: widget.emptyWidget,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 무한 스크롤 그리드 뷰
|
||||
class LazyLoadingGrid<T> extends StatefulWidget {
|
||||
final Future<List<T>> 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<LazyLoadingGrid<T>> createState() => _LazyLoadingGridState<T>();
|
||||
}
|
||||
|
||||
class _LazyLoadingGridState<T> extends State<LazyLoadingGrid<T>> {
|
||||
final List<T> _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<void> _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<void> _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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user