[Flutter] const にできないウィジェットのリビルドを抑制する
目次
Flutter のウィジェットは const コンストラクタを定義していれば、 const 呼び出しでリビルドを抑制することができます。
class _MyWidgetState extends State<MyWidget> {
  int _count = 0;
  void _increment() {
    setState(() {
      _count++;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // ステートが変わってもこの [Text] はリビルドされない
            const Text('You have pushed the button this many times:'),
            Text('$_count'),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}しかし、上記の例でいうと FloatingActionButton は const にできません。なぜなら、関数を渡しているからです。関数オブジェクトは const にできません。
floatingActionButton: const FloatingActionButton(
  onPressed: _increment, // Error: Invalid constant value.
  child: Icon(Icons.add),
),でも渡している関数がずっと同じであるなら、リビルドを抑制できないものかと思うかもしれません。そんなときは対象のウィジェットをキャッシュしておくという方法があります。
class _MyWidgetState extends State<MyWidget> {
  int _count = 0;
  void _increment() {
    setState(() {
      _count++;
    });
  }
  // この [FloatingActionButton] は [_MyWidgetState] の初回ビルド時に
  // 一度だけビルドされる
  late final _fab = FloatingActionButton(
    onPressed: _increment,
    child: const Icon(Icons.add),
  );
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Text('$_count'),
          ],
        ),
      ),
      floatingActionButton: _fab,
    );
  }
}こうすると _count の値が変更されて親ウィジェットがリビルドされても、FloatingActionButton はリビルドされない、という挙動を実現することができます。
ただ、この例での FloatingActionButton や _increment 関数が、親ウィジェットの props を参照している場合は注意が必要です。そのままだと props の値が変わっても古い値を参照し続けてしまうので、キャッシュを更新するひと手間が必要になります。具体的には、StatefulWidget であれば didUpdateWidget ライフサイクルメソッドでキャッシュの更新を行います。
class MyWidget extends StatefulWidget {
  const MyWidget({super.key, required this.incrementAmount});
  final int incrementAmount;
  @override
  _MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
  int _count = 0;
  void _increment() {
    setState(() {
      _count += widget.incrementAmount;
    });
  }
  Widget _buildFab() {
    return FloatingActionButton(
      onPressed: _increment,
      child: const Icon(Icons.add),
    );
  }
  late Widget _fab = _buildFab();
  @override
  void didUpdateWidget(MyWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    // props の値が変わっていたらキャッシュを更新する
    if (widget.incrementAmount != oldWidget.incrementAmount) {
      _fab = _buildFab();
    }
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Text('$_count'),
          ],
        ),
      ),
      floatingActionButton: _fab,
    );
  }
}もし flutter_hooks パッケージを使っていれば、useMemoized や useCallback でもっと簡潔に書くこともできます。
class MyWidget extends HookWidget {
  const MyWidget({super.key, required this.incrementAmount});
  final int incrementAmount;
  @override
  Widget build(BuildContext context) {
    final count = useState(0);
    final increment = useCallback(() {
      count.value += incrementAmount;
    }, [incrementAmount]);
    final fab = useMemoized(() {
      return FloatingActionButton(
        onPressed: increment,
        child: const Icon(Icons.add),
      );
    }, [increment]);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Text('${count.value}'),
          ],
        ),
      ),
      floatingActionButton: fab,
    );
  }
}…とここまで書いてきましたが、このようなキャッシュが実際のパフォーマンスに影響するケースは限定的だと思います。ウィジェットが明らかに重い処理 (どんなものがあるかぱっと思い付きませんが) を走らせているとか、親ウィジェットが文字入力や時間カウント、アニメーションなどに応じて頻繁にリビルドされる時とかでしょうかね。
さんざん巷で言われていることですが、パフォーマンス改善をちゃんとやるならベンチマークやメトリクスの計測を忘れずに!
コメントを残す