【Flutter状態管理解説】change_notifier.dartの仕組みと内部実装を学ぶ【How Flutter Works①】

プロフィール画像

田原 葉

2025年01月28日

リンクをコピーXでシェアするfacebookでシェアする

はじめに 

こんにちは、メンバーズ Cross ApplicationOpen in new tab カンパニーの田原です。

この記事では、Flutterバージョン3.27.2に基づき、Flutter の foundation パッケージ内に含まれるchange_notifier.dart について、コードを追いながら詳しく解説していきます。

change_notifier.dart は、通知処理を担う重要なモジュールであり、ChangeNotifier や ValueNotifier などのクラスが実装されています。このモジュールは、Flutterにおける状態管理の基盤として機能し、オブザーバーパターンをベースにした仕組みを提供します。

change_notifier.dart を理解することは、Flutter や Dart の状態管理を深く理解する上で非常に重要です。

ドキュメントコメントなどの詳細は省略しつつ、重要なポイントをわかりやすく解説していきますので、ぜひ最後までお付き合いください!

▼▼▼実際のリポジトリを読みながら進めたい方はこちらから▼▼▼

コードリーディング 

Flutterのchange_notifier.dartを構成するライブラリ宣言とモジュール構造

library;

import 'dart:ui' show VoidCallback;

import 'package:meta/meta.dart';

import 'assertions.dart';
import 'diagnostics.dart';
import 'memory_allocations.dart';

export 'dart:ui' show VoidCallback;

change_notifier.dartをライブラリとして定義する宣言を行い、ファイル内で使用するモジュールのインポートを行っています。

  • dart:ui:Dartのコアライブラリです、
  • meta.dart:Dartのコードにアノテーション(例: @required, @protected)を追加するためのパッケージです。このパッケージは、コードのメタデータを表現し、静的解析ツールや開発ツールに対して追加情報を提供します。
  • assertions.dart:実行時のアサーションを扱うためのモジュールです。開発中にコードが特定の条件を満たしていることを検証するために使用されます。
  • diagnostics.dart:デバッグ情報や診断を提供するモジュールです。オブジェクトの診断(デバッグやエラー追跡)のために使用されるツール群を提供します。
  • memory_allocations.dart:メモリ割り当てや追跡に関連するモジュールです。

インポートで dart:ui の中から VoidCallback だけを明示的に利用可能にし、エクスポートで他のファイルでこのライブラリをインポートするときに、VoidCallback も利用できるようにしています。

VoidCallback は、引数を取らず戻り値もない関数型のエイリアス(typedef )です。

参考:VoidCallbackOpen in new tab

Listenableクラスの役割を徹底解説!Flutterの通知機能の基礎を学ぶ

abstract class Listenable {
  const Listenable();

  factory Listenable.merge(Iterable<Listenable?> listenables) = _MergingListenable;

  void addListener(VoidCallback listener);

  void removeListener(VoidCallback listener);
}

Listenable クラスは、オブジェクトがリスナーに通知を送ることができる機能を提供するためのものです。ここでいうリスナーとは、オブジェクトの状態が変わったときに呼び出されるコールバック関数(引数を取らず戻り値もない VoidCallBack)のことです。

参考:ListenableOpen in new tab

以下にコードを詳しく見ていきます。

abstract class Listenable {
  const Listenable();

抽象クラス Listenable を定義しています。
Listenableのコンストラクタは、定数コンストラクタとして定義されます。これにより、生成されるオブジェクトをコンパイル時定数にし、同じ引数で生成されたインスタンスが再利用可能になります。

参考:Constant constructorsOpen in new tab

  factory Listenable.merge(Iterable<Listenable?> listenables) = _MergingListenable;

この行はファクトリコンストラクタを定義しています。このコンストラクタは、複数の Listenable オブジェクトをマージし、それらを統合して通知を管理する _MergingListenable というプライベートクラスのインスタンスを返します。

_MergingListenable は Listenable の実装クラスです。詳細は後述します。

参考:Factory constructorsOpen in new tab

  void addListener(VoidCallback listener);

このメソッドは、オブジェクトが通知する際に呼び出すリスナー関数(VoidCallback)を登録するためのメソッドです。

  void removeListener(VoidCallback listener);

このメソッドは、以前に登録したリスナーを削除するためのメソッドです。これを使うことで、特定のリスナーが通知を受け取らないようにすることができます。

ValueListenable<T>の仕組みを理解!単一値の状態管理を実現するクラス

abstract class ValueListenable<T> extends Listenable {
  const ValueListenable();

  T get value;
}

ValueListenable はジェネリック型 T を持つ抽象クラスで、Listenable を継承しています。Listenable クラスは、通知を受け取るリスナーを登録したり削除したりできる機能を提供しますが、 ValueListenable はさらにその「値」を提供する機能を持っています。

value は T 型のゲッターです。ValueListenable クラスを継承した具体的なクラスが、この value を実装して、そのオブジェクトの現在の状態を返すことが期待されます。

参考:ValueListenable<T>Open in new tab

定数宣言(_flutterFoundationLibrary)

const String _flutterFoundationLibrary = 'package:flutter/foundation.dart';

Flutterの foundation.dart パッケージを参照する文字列を宣言しています。

ChangeNotifier

mixin class ChangeNotifier implements Listenable {...}

ChangeNotifier は、Listenable を実装する mixin class で、オブジェクトの状態が変更されたことをリスナーに通知する機能を提供します。

このファイルの主役となるクラスですが、ボリュームが大きいので、次稿で詳述します。

_MergingListenableの実装詳細:複数のListenableを統合する仕組み

class _MergingListenable extends Listenable {
  MergingListenable(this.children);

  final Iterable<Listenable?> _children;

  @override
  void addListener(VoidCallback listener) {
    for (final Listenable? child in _children) {
      child?.addListener(listener);
    }
  }

  @override
  void removeListener(VoidCallback listener) {
    for (final Listenable? child in _children) {
      child?.removeListener(listener);
    }
  }

  @override
  String toString() {
    return 'Listenable.merge([${_children.join(", ")}])';
  }
}

_MergingListenable クラスは、Listenable を継承するプライベートクラスです。このクラスは複数の Listenable を統合する目的にのみ使用されるユーティリティ的な存在です。プライベートにすることで、ライブラリ利用者にとって重要ではない内部的な実装詳細を隠します。

以下に実装の詳細を見ていきます。

class _MergingListenable extends Listenable {
  MergingListenable(this.children);

  final Iterable<Listenable?> _children;

_children は、このクラスが保持する複数の Listenable オブジェクトを格納するための Iterable です。このプロパティには複数の子要素としての Listenable が含まれます。

コンストラクターは _children に格納する、Iterable<Listenable?> を受け取ります。

参考:Iterable<E>Open in new tab
参考:Iterable collectionsOpen in new tab

  @override
  void addListener(VoidCallback listener) {
    for (final Listenable? child in _children) {
      child?.addListener(listener);
    }
  }

このメソッドは、抽象クラス Listenable で定義された、リスナー(VoidCallBack)を追加するメソッドです。
メソッド内部では、for-in-loopを利用して、_children に含まれるすべての Listenable オブジェクト(child)に対して addListener を呼び出します。これにより、_MergingListenable が子として保持するすべての Listenable に同じリスナーが追加されます。

child?.addListener(listener) として、null安全演算子(?)を使用することで、対象の Listenable オブジェクトが null じゃない場合にのみ、addListener が呼び出されます。

  @override
  void removeListener(VoidCallback listener) {
    for (final Listenable? child in _children) {
      child?.removeListener(listener);
    }
  }

このメソッドは、抽象クラス Listenable で定義された、リスナーを削除するための機能を提供します。引数として渡された listener を、_children に含まれる各 Listenable から削除します。

こちらも、child?.removeListener(listener)`という形式で、child が null でない場合にのみリスナーを削除します。

  @override
  String toString() {
    return 'Listenable.merge([${_children.join(", ")}])';
  }

toString メソッドは、Dart のすべてのクラスが暗黙的に継承している基底クラスである Object クラスに定義されている、オブジェクトの文字列表現を返すメソッドです。

ここでは、MergingListenable オブジェクトの文字列表現をカスタマイズするために、オーバーライドしています。
返される文字列は、'Listenable.merge([XXX, OOO, XOX])' という形式になっており、_children の各Listenable をカンマ区切り(", ")で結合して表示します。

参考:ClassesOpen in new tab

ValueNotifierクラスを活用!Flutterの値変更通知を効率化する方法

class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
  ValueNotifier(this._value) {

    if (kFlutterMemoryAllocationsEnabled) {
      ChangeNotifier.maybeDispatchObjectCreation(this);
    }
  }

  @override
  T get value => _value;
  T _value;
  set value(T newValue) {
    if (_value == newValue) {
      return;
    }
    _value = newValue;
    notifyListeners();
  }

  @override
  String toString() => '${describeIdentity(this)}($value)';
}

ValueNotifier<T> は、ChangeNotifier を継承し、ValueListenable<T> を実装するクラスです。このクラスは、特定の値を格納し、その値が変更されたときにリスナーを通知する機能を提供します。

ChangeNotifierは、リスナー(VoidCallback)を登録・削除し、状態の変更時に通知する基本機能を提供します。
ValueListenable<T> は、value プロパティを持ち、その変更をリスナーに通知するためのインターフェースです。

参考:ValueNotifier<T>Open in new tab

以下に実装の詳細を見ていきます。

class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
  ValueNotifier(this._value) {

    if (kFlutterMemoryAllocationsEnabled) {
      ChangeNotifier.maybeDispatchObjectCreation(this);
    }
  }

コンストラクタは、引数に、初期値`_value`を受け取ります。_value は内部で格納される、ジェネリクス T 型の値です。

kFlutterMemoryAllocationsEnabled は、メモリの割り当てを追跡する機能が有効かどうかを示すフラグです。デフォルトでは、デバッグモードでのみ true を返します。

このフラグが有効な場合、ChangeNotifier の静的メソッド maybeDispatchObjectCreation(this) を呼び出します。これは、オブジェクト作成に関する情報を記録するメソッドで、引数に自身のインスタンスを渡しています。

参考:kFlutterMemoryAllocationsEnableOpen in new tab

  @override
  T get value => _value;
  T _value;

value を取得する、ValueListenable<T> に定義されたゲッターをオーバーライドしています。現在の _value の値を返すように実装されています。

_value は T 型のプライベートプロパティで、コンストラクタで初期値が代入され、外部からはアクセスできません。

  set value(T newValue) {
    if (_value == newValue) {
      return;
    }
    _value = newValue;
    notifyListeners();
  }

value のプロパティセッターです。新しい値(newValue)を受け取り、既存の値(_value)と比較します。値が同じ場合には、処理を終了し、通知は行いません。
値が異なる場合には、_value を新しい値に更新し、notifyListeners() を呼び出してリスナーに変更を通知します。

notifyListeners() は ChangeNotifier クラスから継承したもので、登録されたリスナー(VoidCallback)をすべて呼び出して、状態が変更されたことを通知します。

  @override
  String toString() => '${describeIdentity(this)}($value)';
}

基底クラス Object のメソッドをオーバーライドして、オブジェクト名の表示ロジックをカスタマイズしています。

describeIdentity() メソッドに自身のインスタンスを渡して、オブジェクトの識別情報を含めた文字列を取得しています。describeIdentity() メソッドは、foundation パッケージの diagnostics.dart モジュールで定義されています。
戻り値に返す文字列は、describeIdentity() メソッドで取得した文字列の末尾に、現在の value の値を () で括って付与したものです。

参考:describeIdentityOpen in new tab

Flutter状態管理を完全理解!change_notifier.dartから学ぶ基礎と応用

お疲れ様です。
この記事で扱う範囲について、コードリーディングを完了しました。change_notifier.dartを読んだ学びを簡単にまとめます。

Listenable

  • 状態変更の通知ができるオブジェクトのインターフェースとして、通知対象となるVoidCallback関数の追加と削除のメソッドと、複数のListenableをまとめるためのファクトリコンストラクタのみが定義されていました。
  • 「どのように通知するか」についてはインターフェースに定義されておらず、実装者に委ねられる、ミニマムな設計がされていました。

ValueListenable<T>

  • Listenableを継承した、単一の値管理に特化した仕組みを持つオブジェクトのインターフェースとして定義され、任意の型の値を返すためのゲッターが定義されていました。

_MergingListenabl

  • 複数の Listenable をまとめて処理をする仕組みとして、プライベートクラスとして実装されていました。
  • 内部実装では、規定クラス Objectの メソッドをオーバーライドすることで、オブジェクト名を取得する際の表示ロジックをカスタマイズしていました。

ValueNotifier<T>

  • ChangeNotifier を継承してリスナーの追加・削除・通知の仕組みを活かしつつ、ValueListenable<T> インターフェースを実装して単一の値管理に特化したクラス機能を実現していました。
  • また、内部では、kFlutterMemoryAllocationsEnabled フラグを利用して、デバッグモード時のみに有効な機能の管理が行われていました。
  • ここでも、 toString() メソッドをオーバーライドしており、基底クラスを用いた汎用ロジックではなく、個別に最適化した機能を提供する試みが確認できました。

まとめ

この記事では、change_notifier.dart で定義されている Listenable や ValueListenable<T> の抽象クラス、さらにそれらを基にした具体的な実装クラスである _MergingListenable や ValueNotifier<T> について詳しく見てきました。

これらのクラスは、Flutter における状態管理や通知処理の基盤を成しており、Animation やその他の多くのユースケースで活用されています。それぞれがどのように動作し、どのように設計されているのかを理解することで、Flutter の内部構造を深く知ることができます。

なお、ChangeNotifier についてはこの記事では詳細を割愛しましたが、続編で改めてコードリーディングを行い、その詳細を解説する予定です。
ぜひお楽しみに!

最後までお読みいただき、ありがとうございました。

この記事を書いた人

田原 葉
田原 葉
2024年にメンバーズに中途で入社。前職はiOSエンジニア。現在はCross ApplicationカンパニーでFlutter技術をメインにモバイルアプリ開発支援を担当。
ページトップへ戻る