앱 공통 계층 — 데이터 저장소 · 서버 시계 · 푸시 · 결제·광고 · 다국어 5종 · 행성 표현 · 공통 UI · 화면 자산

This commit is contained in:
Sn-Kinos
2026-09-29 17:06:48 +09:00
parent cbfb72efe0
commit c4e9a9430e
165 changed files with 29991 additions and 54 deletions
+64
View File
@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
/// 실시간 구독 한 자리 — [StreamBuilder] 에 **백그라운드 복귀 재구독**을 더한 것.
///
/// 왜 필요한가. Firestore 리스너는 단말이 오래 잠들면 서버와 끊긴 채로 남을 수
/// 있고, 그러면 깨어난 화면이 잠들기 전 값을 그대로 그린다. 별첨 1 ① 예외 처리가
/// 「백그라운드 복귀·화면 회전 시 렌더링 정상 재개」를 판정 항목으로 두었으므로
/// 복귀 시 스트림을 새로 열어 첫 스냅샷을 다시 받는다.
///
/// [create] 는 **구독할 때마다 새 스트림을 만드는 함수**다. 이미 만들어진 스트림을
/// 넘기면 재구독이 같은(이미 닫힌) 스트림을 다시 듣게 되므로 값이 오지 않는다.
///
/// **구독 대상이 바뀌면 [key] 를 바꿔 준다.** [create] 는 빌드마다 새로 만들어지는
/// 클로저라 같은 대상인지 비교할 수 없다. 비교해서 다시 열면 프레임마다 재구독이
/// 돌고, 비교 없이 두면 대상이 바뀌어도 옛 구독이 남는다. 그래서 갈아타기는
/// 위젯 키(예: `ValueKey(fandomId)`)로만 한다.
///
/// 위젯이 사라지면 [StreamBuilder] 가 구독을 끊는다 — 별도 해제 코드가 없는 이유다.
class LiveStreamBuilder<T> extends StatefulWidget {
const LiveStreamBuilder({
super.key,
required this.create,
required this.builder,
});
final Stream<T> Function() create;
/// 값이 아직 없으면 null 로 부른다. 첫 프레임에 빈 자리를 그릴지 로딩을 그릴지는
/// 화면이 정한다 — 배너는 빈 자리가 맞고 목록은 로딩이 맞다.
final Widget Function(BuildContext context, T? value) builder;
@override
State<LiveStreamBuilder<T>> createState() => _LiveStreamBuilderState<T>();
}
class _LiveStreamBuilderState<T> extends State<LiveStreamBuilder<T>>
with WidgetsBindingObserver {
late Stream<T> _stream = widget.create();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
setState(() => _stream = widget.create());
}
}
@override
Widget build(BuildContext context) => StreamBuilder<T>(
stream: _stream,
builder: (context, snap) => widget.builder(context, snap.data),
);
}