본문 바로가기

Flutter

Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active. 에러 원인과 해결 방법

 

 

기능 설명

위 페이지는 `TestPage`이다. 들리는 단어와 일치하는 이미지를 클릭하면 되는 페이지이다. 오디오는 5초마다 한 번씩 재생되어야 하므로 `Timer`와 `AudioPlayer`가 필요하다. 두 객체는 `TestState`에서 관리된다. 

 

 

증상

문제는 다음과 같이 뒤로가기 버튼을 눌렀을 때 오디오를 강제로 종료하는 과정에서 발생하였다. `Navigator`에서 `pop` 되면서 TestPage가 `dispose` 될 때 `Timer`와 `AudioPlayer`를 삭제하려고 시도하자 `Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active.` 라는 에러가 발생하였다. 

onWillPop: () async {
    await Future.delayed(Duration.zero); // UI 업데이트 이후 Navigator 실행
    if (context.mounted) {
      Navigator.popUntil(
        context,
        (route) => route.isFirst, // 모든 기존 화면을 제거
      );
    }
    return false;
},

 

 

원인

라이프 사이클이 다른 state를 호출할 때는 주의가 필요하다. 상대방이 불안정하거나 이미 dispose된 이후일 수도 있기 때문이다. TestState는 아래 코드에서와 같이 위젯 루트인 메인에서 생성된다. 따라서 TestState는 Life cycle 1에 속해있다. 반면 TestPage는 그 하위의 Life cycle 2에 속해 있으므로, TestPage가 dispose() 될 때 TestState가 가지고 있는 timer나 audioPlayer를 stop/dispose 하려고 하면 이미 존재 하지 않거나, 변경되고 있는 참일 수도 있는 것이다.

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.landscapeLeft,
    DeviceOrientation.landscapeRight,
  ]).then((_) {
    runApp(
      MultiProvider(
        providers: [
          ChangeNotifierProvider(create: (_) => AppState()),
          ChangeNotifierProxyProvider<AppState, TestState>(
            create: (context) => TestState(context),
            update: (context, appState, testState) => TestState(context),
          ),
        ],
        child: const MyApp(),
      ),
    );
  });
}

 

 

해결

아래 코드와 같이 didChangeDependencies를 통해 의존하고 있는 State의 변경을 감지하여 업데이트 하도록 하였다. 따라서 dispose()를 할 때 불안정하거나 삭제된 player나 Timer를 호출하지 않고 항상 최신 state를 조회할 수 있게 되었다. 

@override
  void didChangeDependencies() {
    audioTimer = Provider.of<TestState>(context, listen: false).audioTimer;
    player = Provider.of<TestState>(context, listen: false).player;
    super.didChangeDependencies();
  }

  @override
  void dispose() {
    audioTimer?.cancel();
    player.dispose();
    super.dispose();
  }