📘

【Flutter】mountedを使用して確認する代表的なケース

2024/11/17に公開

mountedとは

StatefulWidgetに関連付けられたStateクラスのインスタンスが、現在のウィジェットツリーに含まれているかどうかを確認できるプロパティです。

参考

https://qiita.com/chooyan_eng/items/7c7298795f2db24b24a5

なぜmountedを使用する?

これから代表的なケースをご紹介しますが、無駄な処理を防いでくれたり(パフォーマンス向上)、メモリリークを防いでくれたりします。

参考

https://qiita.com/haru-qiita/items/7aac50973f4aa799ed97#3-パフォーマンスの向上

代表的なケース

非同期データ取得

非同期でデータを取得し、それを表示するウィジェットで、ウィジェットがまだ存在しているかを確認。

例: API からのデータ取得

class AsyncDataWidget extends StatefulWidget {
  
  _AsyncDataWidgetState createState() => _AsyncDataWidgetState();
}

class _AsyncDataWidgetState extends State<AsyncDataWidget> {
  String _data = "Loading...";

  
  void initState() {
    super.initState();
    _fetchData();
  }

  Future<void> _fetchData() async {
    final data = await Future.delayed(Duration(seconds: 2), () => "Fetched Data");
    if (mounted) {
      setState(() {
        _data = data;
      });
    }
  }

  
  Widget build(BuildContext context) {
    return Center(
      child: Text(_data),
    );
  }
}

データ取得完了時にウィジェットが存在していない場合、setState() を呼び出すとエラーになるため。

タイマーを使用する場合

ウィジェットが削除された後もタイマーが動作していると、不適切な状態更新やメモリリークが発生する可能性があります。

例: タイマーでカウントアップ

class TimerWidget extends StatefulWidget {
  
  _TimerWidgetState createState() => _TimerWidgetState();
}

class _TimerWidgetState extends State<TimerWidget> {
  late Timer _timer;
  int _counter = 0;

  
  void initState() {
    super.initState();
    _timer = Timer.periodic(Duration(seconds: 1), (timer) {
      if (mounted) {
        setState(() {
          _counter++;
        });
      } else {
        timer.cancel();
      }
    });
  }

  
  void dispose() {
    _timer.cancel(); // メモリリーク防止
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Text('Counter: $_counter');
  }
}

ウィジェットが削除された後にタイマーが状態変更を試みると、エラーやメモリリークが発生するため。

ストリームをリッスンする場合

データのストリームを監視しながらウィジェットがアンマウントされると、不要なリソース消費が発生します。

例: ストリームを利用したリアルタイム更新

class StreamWidget extends StatefulWidget {
  
  _StreamWidgetState createState() => _StreamWidgetState();
}

class _StreamWidgetState extends State<StreamWidget> {
  late Stream<int> _stream;

  
  void initState() {
    super.initState();
    _stream = Stream.periodic(Duration(seconds: 1), (count) => count).take(10);
    _listenToStream();
  }

  void _listenToStream() {
    _stream.listen((data) {
      if (mounted) {
        setState(() {
          print('Data received: $data');
        });
      }
    });
  }

  
  Widget build(BuildContext context) {
    return Text('Listening to Stream...');
  }
}

ウィジェットが削除された場合でもストリームが動作し続けると、リソースが解放されずメモリリークが発生する可能性があるため。

外部リソースを監視する場合

ウィジェットが削除されても、外部イベントリスナーが呼び出されると不適切な状態変更が行われる可能性があります。

例: ネットワーク接続の監視

class ConnectivityWidget extends StatefulWidget {
  
  _ConnectivityWidgetState createState() => _ConnectivityWidgetState();
}

class _ConnectivityWidgetState extends State<ConnectivityWidget> {
  late StreamSubscription _subscription;
  String _status = "Checking connectivity...";

  
  void initState() {
    super.initState();
    _subscription = Connectivity().onConnectivityChanged.listen((status) {
      if (mounted) {
        setState(() {
          _status = status.toString();
        });
      }
    });
  }

  
  void dispose() {
    _subscription.cancel(); // イベントリスナーを解放
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Text(_status);
  }
}

ウィジェットが削除された後にイベントが発生すると、状態を更新しようとしてエラーになる可能性があるため。

モーダルダイアログや画面遷移を挟む場合

ウィジェットが画面外に移動していても、非同期操作が続いていると不適切な状態更新が発生します。

例: ダイアログを閉じた後の処理

class ModalWidget extends StatefulWidget {
  
  _ModalWidgetState createState() => _ModalWidgetState();
}

class _ModalWidgetState extends State<ModalWidget> {
  String _message = "Awaiting response...";

  Future<void> _showDialog() async {
    final result = await showDialog<String>(
      context: context,
      builder: (context) => AlertDialog(
        title: Text("Confirmation"),
        content: Text("Do you confirm?"),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context, "Yes"), child: Text("Yes")),
          TextButton(onPressed: () => Navigator.pop(context, "No"), child: Text("No")),
        ],
      ),
    );

    if (mounted) {
      setState(() {
        _message = "User selected: $result";
      });
    }
  }

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(_message),
        ElevatedButton(onPressed: _showDialog, child: Text("Show Dialog")),
      ],
    );
  }
}

ダイアログが閉じられた後に、元のウィジェットが削除されている場合、非同期の結果を処理するとエラーになるため。

アニメーションやコントローラの利用時

AnimationController などを使ったアニメーションが、ウィジェット削除後も動作し続けると不適切な状態になる場合があります。

例: アニメーションを使うケース

class AnimationWidget extends StatefulWidget {
  
  _AnimationWidgetState createState() => _AnimationWidgetState();
}

class _AnimationWidgetState extends State<AnimationWidget> with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: Duration(seconds: 2))
      ..addListener(() {
        if (mounted) {
          setState(() {});
        }
      });
    _controller.forward();
  }

  
  void dispose() {
    _controller.dispose(); // コントローラを解放
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Opacity(
      opacity: _controller.value,
      child: Text("Animating..."),
    );
  }
}

ウィジェットが削除されても AnimationController が動作し続けると、状態変更が試みられエラーやメモリリークの原因になるため。

まとめ

ケース 具体例 理由
非同期データ取得 API 呼び出しやデータロード 削除されたウィジェットで setState を呼び出すエラーを防ぐ
タイマー利用 定期的な状態更新 メモリリーク防止、不要な状態更新を防ぐ
ストリーム監視 リアルタイムデータの受信 不適切なリソース消費やエラーを防ぐ
外部リソースの監視 ネットワークやデバイスイベントのリスニング メモリ管理と安全な状態更新を保証
モーダルや画面遷移後の処理 ダイアログや遷移後の結果処理 非同期処理後に削除されたウィジェットでエラーを防ぐ
アニメーションやコントローラ利用 AnimationController, GestureDetector など 状態変更エラーやメモリリークを防ぐ

他にもこんなケースでよく使用するとかございましたら、教えていただけると嬉しいです!

https://api.flutter.dev/flutter/widgets/State/mounted.html

Discussion