[Flutter]非同期処理での注意点 setStateとdispose

スポンサーリンク

エラーメッセージを読めばわかりますが、解決策を残しておきます。

結論

  • エラーメッセージはちゃんと読もう

背景

ある画面で、Flutterの cameraパッケージの CameraController.startImageStreamを使い、定期的に Imageデータを取得して、ネイティブ側に渡して非同期処理を行っていた。
非同期処理は、setState()でネイティブ側の処理の結果を受けるようにしていた。

終了処理dispose()が呼ばれた後にも、setState()が呼ばれた際に、以下のエラーメッセージが表示された。

※この画面からの画面遷移で、発生していました。

エラーメッセージ

原文

Unhandled Exception: setState() called after dispose(): _CameraAppState#fde41(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().
 #0      State.setState.<anonymous closure> (package:flutter/src/widgets/framework.dart:1112:9)
 

グーグル翻訳

処理の例外:dispose()の後に呼び出されるsetState():_CameraAppState#fde41(ライフサイクル状態:無効、マウントされていません)
 このエラーは、ウィジェットツリーに表示されなくなったウィジェットのStateオブジェクトでsetState()を呼び出すと発生します(たとえば、親ウィジェットのビルドにウィジェットが含まれなくなった場合)。このエラーは、コードがタイマーまたはアニメーションコールバックからsetState()を呼び出すときに発生する可能性があります。
 推奨される解決策は、タイマーをキャンセルするか、dispose()コールバックでアニメーションのリッスンを停止することです。別の解決策は、setState()を呼び出す前にこのオブジェクトの「mounted」プロパティをチェックして、オブジェクトがツリー内にあることを確認することです。
 このエラーは、ツリーから削除された後、別のオブジェクトがこのStateオブジェクトへの参照を保持しているため、setState()が呼び出されている場合にメモリリークを示している可能性があります。メモリリークを回避するには、dispose()中にこのオブジェクトへの参照を解除することを検討してください。
 #0 State.setState。<匿名クロージャ>(パッケージ:flutter / src / widgets / framework.dart:1112:9)

解決策

エラーメッセージ通り、setState()を呼び出す前にこのオブジェクトの「mounted」プロパティをチェックする。

コード

修正前

  @override
  void initState() {
    super.initState();
    getCameras().then((_) {
      controller.initialize().then((_) {
        print("deb::controller.initialize");
        if (!mounted) {
          return;
        }

        controller.startImageStream((CameraImage availableImage) {
          _scanText(availableImage);
        });
        setState(() {});
      });
    });
  }

  void _scanText(CameraImage availableImage) async {
    String barcode = await platform.invokeMethod('web', {
      "bytes": availableImage.planes[0].bytes,
      "height": availableImage.height,
      "width": availableImage.width
    });
    setState(() {
      if (barcode.isNotEmpty) {
        controller.stopImageStream();
        qrText = barcode;
      }
    });
  }

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

エラーになるケース

  • startImageStreamにより、定期的に、_scanText()が呼ばれる
  • _scanText()内の”await platform.invokeMethod”が返ってくる前に、画面遷移などにより dispose()が呼ばれる
  • その後に、await処理が終了して、setState()が呼ばれる

修正後

  @override
  void initState() {
    super.initState();
    getCameras().then((_) {
      controller.initialize().then((_) {
        print("deb::controller.initialize");
        if (!mounted) {
          return;
        }

        controller.startImageStream((CameraImage availableImage) {
          _scanText(availableImage);
        });
        setState(() {});
      });
    });
  }

  void _scanText(CameraImage availableImage) async {
    String barcode = await platform.invokeMethod('web', {
      "bytes": availableImage.planes[0].bytes,
      "height": availableImage.height,
      "width": availableImage.width
    });
///// ここ追加 /////
    if (!mounted) {
      return;
    }
////////////////////
    setState(() {
      if (barcode.isNotEmpty) {
        controller.stopImageStream();
        qrText = barcode;
      }
    });
  }

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

参考

https://blog.mrym.tv/2019/12/traps-on-calling-setstate-inside-initstate/

コメント

タイトルとURLをコピーしました