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:
JiWoong Sul
2025-07-10 18:36:57 +09:00
parent 8619e96739
commit 4731288622
55 changed files with 8219 additions and 2149 deletions

View 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,
);
},
);
}
}