【Flutter】ValueListenableBuilderによる状態管理

対象者

  • Flutterを使ったアプリ開発に取り組んでいるエンジニア
  • 状態管理を効率的に行いたいと考えている方
  • ValueListenableBuilderの使い方や応用方法を学びたい方

はじめに

Flutterアプリ開発において、状態管理は非常に重要な要素です。効率的な状態管理を実現するためには、適切なツールや方法が求められます。そんな中、ValueListenableBuilderは、状態管理において非常に有用なウィジェットの一つです。この記事では、ValueListenableBuilderの基本的な使い方から応用方法まで、わかりやすく解説していきます。また、実践例としてカウンターアプリケーションのコード例や解説もご紹介します。

https://www.youtube.com/watch?v=IJt8BZkc_II&feature=youtu.be

ValueListenableBuilderの概要

ValueListenableBuilderとは何か

ValueListenableBuilderは、Flutterで用意されたウィジェットで、ValueListenableオブジェクトの値が変化したときにウィジェットを自動的に再構築します。ValueNotifierやAnimationなど、ValueListenableインターフェースを実装したオブジェクトと組み合わせて使われることが多いです。これにより、データの変更に応じてウィジェットを更新することが容易になります。

ValueListenableBuilderの利点

ValueListenableBuilderを使うことで、以下のような利点があります。

  • 状態の変更に応じてウィジェットを効率的に再構築できるため、アプリのパフォーマンスが向上します。
  • コードの可読性が高まり、データの変更に応じたウィジェットの更新処理をシンプルに記述できます。
  • ValueNotifierやAnimationといったValueListenableオブジェクトとの連携が容易で、これらのオブジェクトが変更を通知するたびに、ValueListenableBuilderは自動的にウィジェットを再構築します。

ValueListenableBuilderの基本的な使い方

ValueNotifierとの組み合わせ

ValueNotifierは、簡単な状態管理を実現するためのクラスです。ValueListenableBuilderと組み合わせることで、ValueNotifierが持つデータの変更に応じたUIの更新を効率的に行うことができます。

ValueNotifier<int> _counter = ValueNotifier<int>(0);

ValueListenableBuilder<int>(
  valueListenable: _counter,
  builder: (context, value, child) {
    return Text('$value');
  },
)

Animationとの組み合わせ

FlutterのAnimationは、アニメーションの状態を保持し、変更を監視するためのクラスです。ValueListenableBuilderと組み合わせることで、アニメーションの状態変化に応じてUIを効率的に更新することができます。

AnimationController _controller = AnimationController(
  vsync: this,
  duration: const Duration(seconds: 2),
)..repeat(reverse: true);

ValueListenableBuilder<double>(
  valueListenable: _controller,
  builder: (context, value, child) {
    return Opacity(
      opacity: value,
      child: child,
    );
  },
  child: FlutterLogo(size: 100),
)

実践例: カウンターアプリケーション

コード例

 final ValueNotifier<int> _counter = ValueNotifier<int>(0);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('You have pushed the button this many times:'),
          ValueListenableBuilder(
            valueListenable: _counter,
            builder: (context, value, child) {
              return Text(
                '$value',
                style: Theme.of(context).textTheme.headline4,
              );
            },
          ),
          ElevatedButton(
            onPressed: () => _counter.value++,
            child: Text('Increment'),
          ),
        ],
      ),
    );
  }

解説

このカウンターアプリケーションでは、ValueNotifierを用いてカウンターの値を管理しています。カウンターの値は、CounterWidgetクラスのstaticフィールドに格納されています。

FloatingActionButtonのonPressedイベントでカウンターの値をインクリメントしています。このとき、ValueNotifierの値が変更されると、ValueListenableBuilderがリビルドを行い、新しいカウンターの値が表示されます。

このように、ValueListenableBuilderとValueNotifierを組み合わせることで、状態変更に応じたUIの更新を効率的かつ簡潔なコードで実現できます。

ValueListenableBuilderのメリット・デメリット

ValueListenableBuilderのメリット

  • シンプルなAPI: ValueListenableBuilderは非常にシンプルなAPIを持っており、使い方を学ぶのが容易です。これは、新しいフレームワークやライブラリに対する学習曲線を緩和するのに役立ちます。

  • 効率的なリスニング: ValueListenableBuilderは、リスニングの対象となる特定のValueListenableオブジェクトが変更されたときだけウィジェットツリーを再構築します。これにより、不必要な再レンダリングが削減され、アプリケーションのパフォーマンスが向上します。

  • 直感的なデータフロー: ValueListenableBuilderを使用すると、アプリケーションの状態管理が一元化され、データの流れが直感的になります。これにより、コードの読みやすさと保守性が向上します。

ValueListenableBuilderのデメリット

  • スケーリングの制限: ValueListenableBuilderはシンプルで使いやすいですが、大規模なプロジェクトや複雑な状態管理のニーズに対応するのは困難かもしれません。例えば、複数のValueListenableオブジェクトを一元的に管理するのは難しいです。

  • リアクティブプログラミングの欠如: ValueListenableBuilderは、リアクティブプログラミングパラダイムをフルに活用することが難しいです。これは、一部の開発者にとっては制限となり得ます。

他の状態管理ツール(Riverpod, GetX, setState等)と比較して、ValueListenableBuilderはAPIのシンプルさと効率的なリスニングに優れていますが、スケーラビリティやリアクティブプログラミングのサポートでは劣る可能性があります。プロジェクトの要件やチームの経験によって最適なツールが異なるため、各ツールの特性を理解した上で選択することが重要です。

まとめ

Flutter開発において、ValueListenableBuilderを効果的に使いこなすことで、状態管理がシンプルかつ効率的になりました。以下のポイントが特に役立ちました。

ValueListenableBuilderを使ってUIの更新を効率化
ValueNotifierとの組み合わせで状態管理を簡素化
パフォーマンスの最適化が容易になる

今回この状態管理に初めて触れましたが、やっぱRiverpodでいいかなぁ、という気がします(笑)

参考

ソース(main.dartにコピペして動作確認用)

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('ValueListenableBuilder Sample'),
        ),
        body: MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  final ValueNotifier<int> _counter = ValueNotifier<int>(0);
  late AnimationController _controller = AnimationController(
    vsync: this,
    duration: const Duration(seconds: 2),
  )..repeat(reverse: true);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('You have pushed the button this many times:'),
          ValueListenableBuilder(
            valueListenable: _counter,
            builder: (context, value, child) {
              return Text(
                '$value',
                style: Theme.of(context).textTheme.headline4,
              );
            },
          ),
          ValueListenableBuilder<int>(
            valueListenable: _counter,
            builder: (context, value, child) {
              return Text('$value');
            },
          ),
          ElevatedButton(
            onPressed: () => _counter.value++,
            child: Text('Increment'),
          ),
          SizedBox(height: 48),
          ValueListenableBuilder<double>(
            valueListenable: _controller,
            builder: (context, value, child) {
              return Opacity(
                opacity: value,
                child: child,
              );
            },
            child: FlutterLogo(size: 100),
          )
        ],
      ),
    );
  }

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