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