guricode

[flutter]자취의 정석 -7 커뮤니티 목록 안 뜸 / ref dispose 오류 본문

앱/Flutter&Dart

[flutter]자취의 정석 -7 커뮤니티 목록 안 뜸 / ref dispose 오류

agentrakugaki 2025. 9. 29. 04:56

증상

  • 게시글 리스트가 로드되지 않거나 계속 로딩.
  • 콘솔에 Bad state: Cannot use "ref" after the widget was disposed. 발생.
  • WidgetsBinding.instance.addPostFrameCallback로 loadInitial 호출 시에도 간헐적으로 동일.

원인

  1. build() 안에서 provider를 새로 생성 → initState()에서 로드한 provider와 다른 인스턴스를 watch 하게 되어 초기 로드가 해당 인스턴스에 적용되지 않음.
  2. 위치(location)가 나중에 들어오는 비동기 흐름을 고려하지 않음 → provider를 만들 시점이 불명확.
  3. 프레임 콜백/스케줄러 타이밍에 ref 접근 → 위젯 dispose 이후 실행되면 ref 사용 불가 오류.

해결

  • provider를 필드로 한 번만 생성하고,
    initState() / didUpdateWidget()에서만 초기화 + 1회 로드.
  • build()에서는 이미 만든 provider만 watch.
  • location이 나중에 들어오면 didUpdateWidget()에서 provider를 재초기화.
  • addPostFrameCallback 제거(필요 없음).

수정 코드 (핵심 부분)

class _PostsPlaceholderState extends ConsumerState<_PostsPlaceholder> {
  // build에 의존하지 않는 고정 provider
  late NotifierProvider<CommunityListVM, CommunityListState> provider;
  bool _ready = false; // provider 준비 여부

  // 댓글수 캐시
  final Map<String, Future<int>> _commentCountFutures = {};

  @override
  void initState() {
    super.initState();
    _maybeInitProviderAndLoad();
  }

  @override
  void didUpdateWidget(covariant _PostsPlaceholder oldWidget) {
    super.didUpdateWidget(oldWidget);
    // location이 뒤늦게 생기거나 변경되면 재초기화
    if (oldWidget.location != widget.location) {
      _ready = false;
      _maybeInitProviderAndLoad();
    }
  }

  void _maybeInitProviderAndLoad() {
    final loc = widget.location;
    if (loc == null || loc.isEmpty) return; // 아직 준비 안 됨

    provider = communityListVmProvider(
      categoryCode: widget.parentCode,
      detailCode: widget.detailCode,
      location: loc,
    );
    _ready = true;

    // 초기 로드 1회
    Future.microtask(() {
      if (!mounted) return;
      ref.read(provider.notifier).loadInitial(ref);
    });
  }

  @override
  Widget build(BuildContext context) {
    if (!_ready) return const NoLocationView(); // 위치 없거나 아직 준비 전

    // ★ build에서는 필드 provider만 watch (다시 만들지 말기!)
    final st = ref.watch(provider);

    return Scaffold(
      body: NotificationListener<ScrollNotification>(
        onNotification: (n) {
          if (!st.hasMore || st.isLoading) return false;
          if (n.metrics.pixels >= n.metrics.maxScrollExtent * 0.9) {
            ref.read(provider.notifier).loadMore(ref);
          }
          return false;
        },
        child: Column(
          children: [
            // ... 기존 헤더/정렬 UI ...
            Expanded(
              child: ListView.separated(
                padding: const EdgeInsets.only(left: 24, right: 24, bottom: 100),
                itemCount: st.items.length + ((st.isLoading && st.hasMore) ? 1 : 0),
                separatorBuilder: (_, __) => const SizedBox(height: 12),
                itemBuilder: (_, i) {
                  if (i >= st.items.length) {
                    return const Center(
                      child: Padding(
                        padding: EdgeInsets.all(16),
                        child: CircularProgressIndicator(),
                      ),
                    );
                  }
                  final x = st.items[i];
                  // ... 기존 셀 UI (NickName, 댓글 수 Future, 날짜 등) ...
                  // _commentCountFutures 활용 로직 그대로 유지
                  return /* 기존 InkWell 카드 */;
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

체크리스트

  • provider를 build에서 생성하지 않는다.
  • 초기 로드는 initState / didUpdateWidget에서 1회만.
  • 비동기 값(location)이 바뀌면 provider 재초기화 후 다시 loadInitial.
  • addPostFrameCallback로 ref를 늦게 호출하지 않는다(불필요/위험).