perf: 파싱/렌더 최적화 다수 적용

- SmsScanner 키워드/정규식 상수화로 반복 컴파일 제거\n- 리스트에 prototypeItem 추가, 카드 RepaintBoundary 적용\n- 차트 영역 RepaintBoundary로 페인트 분리\n- GlassmorphicScaffold 파티클 수를 disableAnimations에 따라 감소\n- 캐시 초기화 플래그를 --dart-define로 제어(CLEAR_CACHE_ON_STARTUP)
This commit is contained in:
JiWoong Sul
2025-09-07 23:28:18 +09:00
parent d37f66d526
commit 84b3fdd530
7 changed files with 203 additions and 188 deletions

View File

@@ -154,99 +154,101 @@ class MonthlyExpenseChartCard extends StatelessWidget {
),
),
const SizedBox(height: 20),
// 바 차트
AspectRatio(
aspectRatio: 1.6,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: _calculateChartMaxY(
monthlyData.fold<double>(
0,
(max, data) => math.max(
max, data['totalExpense'] as double)),
locale),
barGroups: _getMonthlyBarGroups(locale),
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: _calculateGridInterval(
_calculateChartMaxY(
monthlyData.fold<double>(
0,
(max, data) => math.max(max,
data['totalExpense'] as double)),
locale),
CurrencyUtil.getDefaultCurrency(locale)),
getDrawingHorizontalLine: (value) {
return FlLine(
color:
AppColors.navyGray.withValues(alpha: 0.1),
strokeWidth: 1,
);
},
),
titlesData: FlTitlesData(
show: true,
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
// 바 차트 (RepaintBoundary로 페인트 분리)
RepaintBoundary(
child: AspectRatio(
aspectRatio: 1.6,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: _calculateChartMaxY(
monthlyData.fold<double>(
0,
(max, data) => math.max(
max, data['totalExpense'] as double)),
locale),
barGroups: _getMonthlyBarGroups(locale),
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: _calculateGridInterval(
_calculateChartMaxY(
monthlyData.fold<double>(
0,
(max, data) => math.max(max,
data['totalExpense'] as double)),
locale),
CurrencyUtil.getDefaultCurrency(locale)),
getDrawingHorizontalLine: (value) {
return FlLine(
color:
AppColors.navyGray.withValues(alpha: 0.1),
strokeWidth: 1,
);
},
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
return Padding(
padding: const EdgeInsets.only(top: 8),
child: ThemedText.caption(
text: monthlyData[value.toInt()]
['monthName'],
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
titlesData: FlTitlesData(
show: true,
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
return Padding(
padding: const EdgeInsets.only(top: 8),
child: ThemedText.caption(
text: monthlyData[value.toInt()]
['monthName'],
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
);
},
),
),
leftTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(show: false),
barTouchData: BarTouchData(
enabled: true,
touchTooltipData: BarTouchTooltipData(
tooltipBgColor: AppColors.darkNavy,
tooltipRoundedRadius: 8,
getTooltipItem:
(group, groupIndex, rod, rodIndex) {
return BarTooltipItem(
'${monthlyData[group.x]['monthName']}\n',
const TextStyle(
color: AppColors.pureWhite,
fontWeight: FontWeight.bold,
),
children: [
TextSpan(
text: CurrencyUtil
.formatTotalAmountWithLocale(
monthlyData[group.x]
['totalExpense'] as double,
locale),
style: const TextStyle(
color: Color(0xFFFBBF24),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
);
},
),
),
leftTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(show: false),
barTouchData: BarTouchData(
enabled: true,
touchTooltipData: BarTouchTooltipData(
tooltipBgColor: AppColors.darkNavy,
tooltipRoundedRadius: 8,
getTooltipItem:
(group, groupIndex, rod, rodIndex) {
return BarTooltipItem(
'${monthlyData[group.x]['monthName']}\n',
const TextStyle(
color: AppColors.pureWhite,
fontWeight: FontWeight.bold,
),
children: [
TextSpan(
text: CurrencyUtil
.formatTotalAmountWithLocale(
monthlyData[group.x]
['totalExpense'] as double,
locale),
style: const TextStyle(
color: Color(0xFFFBBF24),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
);
},
),
),
),
),

View File

@@ -177,10 +177,13 @@ class _GlassmorphicScaffoldState extends State<GlassmorphicScaffold>
child: AnimatedBuilder(
animation: _particleController,
builder: (context, child) {
final media = MediaQuery.maybeOf(context);
final reduce = media?.disableAnimations ?? false;
final count = reduce ? 10 : 30;
return CustomPaint(
painter: ParticlePainter(
animation: _particleController,
particleCount: 30,
particleCount: count,
),
);
},

View File

@@ -71,6 +71,7 @@ class SubscriptionListWidget extends StatelessWidget {
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: const EdgeInsets.symmetric(horizontal: 16),
prototypeItem: const SizedBox(height: 156),
itemCount: subscriptions.length,
itemBuilder: (context, subIndex) {
// 각 구독의 지연값 계산 (순차적으로 나타나도록)
@@ -98,60 +99,63 @@ class SubscriptionListWidget extends StatelessWidget {
child: StaggeredAnimationItem(
index: subIndex,
delay: const Duration(milliseconds: 50),
child: SwipeableSubscriptionCard(
subscription: subscriptions[subIndex],
onTap: () {
Log.d(
'[SubscriptionListWidget] SwipeableSubscriptionCard onTap 호출됨');
AppNavigator.toDetail(
context, subscriptions[subIndex]);
},
onDelete: () async {
// 현재 로케일에 맞는 서비스명 가져오기
final localeProvider =
Provider.of<LocaleProvider>(
context,
listen: false,
);
final locale =
localeProvider.locale.languageCode;
final displayName = await SubscriptionUrlMatcher
.getServiceDisplayName(
serviceName:
subscriptions[subIndex].serviceName,
locale: locale,
);
// 삭제 확인 다이얼로그 표시
if (!context.mounted) return;
final shouldDelete =
await DeleteConfirmationDialog.show(
context: context,
serviceName: displayName,
);
if (!context.mounted) return;
if (shouldDelete) {
// 사용자가 확인한 경우에만 삭제 진행
final provider =
Provider.of<SubscriptionProvider>(
child: RepaintBoundary(
child: SwipeableSubscriptionCard(
subscription: subscriptions[subIndex],
onTap: () {
Log.d(
'[SubscriptionListWidget] SwipeableSubscriptionCard onTap 호출됨');
AppNavigator.toDetail(
context, subscriptions[subIndex]);
},
onDelete: () async {
// 현재 로케일에 맞는 서비스명 가져오기
final localeProvider =
Provider.of<LocaleProvider>(
context,
listen: false,
);
await provider.deleteSubscription(
subscriptions[subIndex].id,
final locale =
localeProvider.locale.languageCode;
final displayName =
await SubscriptionUrlMatcher
.getServiceDisplayName(
serviceName:
subscriptions[subIndex].serviceName,
locale: locale,
);
if (context.mounted) {
AppSnackBar.showError(
context: context,
message: AppLocalizations.of(context)
.subscriptionDeleted(displayName),
icon: Icons.delete_forever_rounded,
// 삭제 확인 다이얼로그 표시
if (!context.mounted) return;
final shouldDelete =
await DeleteConfirmationDialog.show(
context: context,
serviceName: displayName,
);
if (!context.mounted) return;
if (shouldDelete) {
// 사용자가 확인한 경우에만 삭제 진행
final provider =
Provider.of<SubscriptionProvider>(
context,
listen: false,
);
await provider.deleteSubscription(
subscriptions[subIndex].id,
);
if (context.mounted) {
AppSnackBar.showError(
context: context,
message: AppLocalizations.of(context)
.subscriptionDeleted(displayName),
icon: Icons.delete_forever_rounded,
);
}
}
}
},
},
),
),
),
),