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