[Flutter] setState() called after dispose()

·

2 min read

경고 메세지 분석

Unhandled Exception: setState() called after dispose(): _NoMoreItemState#dc583(lifecycle state: defunct, not mounted)

This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback.

The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.

This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().

앱에 타이머를 추가하거나, 애니메이션 위젯을 구현하다보면 종종 발견할 수 있는 warning 메세지이다. setState()가 실행되는 시점에 이미 해당 위젯이 dispose()된 상태인 경우, 이는 메모리 누수가 발생할 수 있다는 warning 메시지가 발생하게 된다.

이를 해결하기 위해서는 Timer를 onDispose()시 함께 dispose 해주거나 해당 위젯이 mounted인 상태일 경우에만 setState()를 호출할 수 있도록 로직을 수정해주어야한다.

다음 예제는 아이템 리스트를 불러오다가 마지막 아이템을 읽어온 후에 해당 아이템이 마지막 아이템임을 표시해주는 Text() 위젯이 AnimatedOpacity()위젯을 활용해 애니메이션 효과와 함께 출력되도록 하는 로직이다.


class NoMoreItem extends StatefulWidget {
  final String? label;
  const NoMoreItem({super.key, this.label});

  @override
  State<NoMoreItem> createState() => _NoMoreItemState();
}

class _NoMoreItemState extends State<NoMoreItem> with AfterLayoutMixin<NoMoreItem> {
  var _op = 0.0;

  @override
  FutureOr<void> afterFirstLayout(BuildContext context) async {
    await Future.delayed(const Duration(milliseconds: 400), () {
      if (mounted) {
        setState(() {
          _op = 1.0;
        });
      }
    });
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      margin: const EdgeInsets.only(top: 16, bottom: 10),
      alignment: Alignment.center,
      child: AnimatedOpacity(
        duration: const Duration(milliseconds: 400),
        opacity: _op,
        child: Text(
          widget.label ?? 'That\'s all for now!'.hardcoded,
          style: AppTextStyles.black.copyWith(
            color: Palette.gray6B,
            fontSize: 17.sp,
            fontWeight: FontWeight.w600,
          ),
        ),
      ),
    );
  }
}

예제에서 사용한 AfterLayout과 관련된 내용은 패키지 설명을 통해 확인할 수 있다.

After Layout Package

예제에서는 afterLayout() 내에서 위젯이 위젯 트리에 생성되고 UI가 display될 때 애니메이션이 실행될 수 있도록 일정한 딜레이를 주고 setState()를 실행한다.

@override
  FutureOr<void> afterFirstLayout(BuildContext context) async {
    await Future.delayed(const Duration(milliseconds: 400), () {
      if (mounted) {
        setState(() {
          _op = 1.0;
        });
      }
    });
  }

해당 부분에서 만약 mounted를 체크해주지 않고 setState()를 호출하게 되면 만약 사용자가 setState()가 실행되는 시점에 다른 페이지로 이동하는 경우가 발생하여 dispose()가 일어나게 될 경우 setState() called after dispose() 경고가 출력될 수 있으며, 메모리 누수가 발생할 수 있다.

if(mounted)를 통해 분기처리를 해주면 setState()함수가 위젯이 dispose()되지 않은 경우에만 실행되도록 설정할 수 있다.

edited by 김동한