perf(game): setState 전체 리빌드 → ValueNotifier 국소 리빌드

- GameState를 ValueNotifier로 감싸 50ms마다 전체 트리 리빌드 방지
- build()에서 ValueListenableBuilder 사용
- _specialAnimation 등 UI 전용 상태만 setState 유지
- SchedulerBinding 의존성 제거
This commit is contained in:
JiWoong Sul
2026-03-30 20:43:28 +09:00
parent 9be0dd3e4f
commit a2496d219e

View File

@@ -1,7 +1,6 @@
import 'package:flutter/foundation.dart'
show kIsWeb, defaultTargetPlatform, TargetPlatform;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show SchedulerBinding, SchedulerPhase;
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
import 'package:asciineverdie/src/core/infrastructure/iap_service.dart';
@@ -60,6 +59,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
with WidgetsBindingObserver {
AsciiAnimationType? _specialAnimation;
// 게임 상태를 국소적으로 갱신하기 위한 ValueNotifier
// (매 틱마다 전체 위젯 트리 리빌드 방지)
final _stateNotifier = ValueNotifier<GameState?>(null);
// Phase 8: 알림 서비스 (Notification Service)
late final NotificationService _notificationService;
@@ -207,8 +210,9 @@ class _GamePlayScreenState extends State<GamePlayScreen>
widget.controller.addListener(_onControllerChanged);
WidgetsBinding.instance.addObserver(this);
// 초기 상태 설정
// 초기 상태 설정 (ValueNotifier 포함)
final state = widget.controller.state;
_stateNotifier.value = state;
if (state != null) {
_lastLevel = state.traits.level;
_lastQuestCount = state.progress.questCount;
@@ -242,6 +246,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
@override
void dispose() {
_stateNotifier.dispose();
_notificationService.dispose();
WidgetsBinding.instance.removeObserver(this);
widget.controller.removeListener(_onControllerChanged);
@@ -355,17 +360,9 @@ class _GamePlayScreenState extends State<GamePlayScreen>
_checkSpecialEvents(state);
}
// WASM 안정성: 프레임 빌드 중이면 다음 프레임까지 대기
if (SchedulerBinding.instance.schedulerPhase ==
SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {});
}
});
} else {
setState(() {});
}
// ValueNotifier로 게임 상태만 국소 갱신 (전체 setState 제거)
// _checkSpecialEvents 내부의 setState(_specialAnimation)는 유지
_stateNotifier.value = state;
}
/// 캐로셀 레이아웃 사용 여부 판단
@@ -531,18 +528,26 @@ class _GamePlayScreenState extends State<GamePlayScreen>
@override
Widget build(BuildContext context) {
final state = widget.controller.state;
if (state == null) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
// ValueListenableBuilder로 게임 상태 변경만 국소 리빌드
// (_specialAnimation 등 UI 전용 상태는 여전히 setState로 관리)
return ValueListenableBuilder<GameState?>(
valueListenable: _stateNotifier,
builder: (context, state, _) {
if (state == null) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
final localeKey = ValueKey(game_l10n.currentGameLocale);
final localeKey = ValueKey(game_l10n.currentGameLocale);
if (_shouldUseCarouselLayout(context)) {
return _buildMobileLayout(context, state, localeKey);
}
if (_shouldUseCarouselLayout(context)) {
return _buildMobileLayout(context, state, localeKey);
}
return _buildDesktopLayout(context, state, localeKey);
return _buildDesktopLayout(context, state, localeKey);
},
);
}
/// 모바일 캐로셀 레이아웃