65 lines
2.5 KiB
Dart
65 lines
2.5 KiB
Dart
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),
|
|
);
|
|
}
|