はじめに (対象読者・この記事でわかること)

このブログは、Flutter (Dart) で開発を始めたばかりのエンジニアや、既に実務で使っているが「文字列や数値を関数に渡すと変更できない」ことに悩んでいる方を対象としています。
この記事を読むことで、以下のことができるようになります。

  1. Dart の「値渡し」と「参照渡し」の違いを正しく理解する。
  2. プリミティブ型(intString など)を参照渡し的に扱うラッパークラスの作り方。
  3. ValueNotifierChangeNotifier を使ったリアクティブな状態管理と、UI への反映方法。

背景として、Flutter の UI は状態変化に敏感に反応させる必要があるものの、Dart の言語仕様上、プリミティブはイミュータブルであるため「参照渡し」ができません。このギャップを埋める具体的な手法を紹介します。


前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • Flutter の基本的なプロジェクト構造と StatefulWidget の概念
  • Dart のクラス定義・ジェネリクスの基礎
  • setStateProvider といった状態管理の基本的な流れ

1. なぜ「参照渡し」が必要なのか ― 背景と概念整理

Dart では、変数に格納されたオブジェクトの参照自体が として渡されます。つまり、関数に intString を渡すと コピー が作られ、関数内部でそのコピーを変更しても呼び出し元の変数は変化しません。

Dart
void increment(int x) { x = x + 1; // コピーを変更しているだけ }

この挙動は UI のリアクティブな更新にとって不便です。たとえば、ボタンを押したときにカウンタを増やしたいだけなのに、毎回新しい変数を返す構造にするとコードが冗長になります。

参照渡し的に扱えるようにする主な手段は次の二つです。

  1. ミュータブル(可変)オブジェクトでラップする
  2. ValueNotifier / ChangeNotifier といった Flutter の状態管理仕組みを利用する

このセクションでは、まず「ラップクラス」の概念を簡単に説明し、その後で ValueNotifier を使った実装例へと進みます。


2. 具体的な手順や実装方法

以下では、ステップごとにコード例と注意点を示します。全体の流れは次の通りです。

  1. プリミティブ型をラップする汎用クラス Ref<T> を作成
  2. Ref<T> を使って関数に参照渡し風にデータを渡す
  3. UI 側で ValueNotifier<Ref<T>> を活用し、変更を自動的に反映
  4. ハマりポイントとその対策

ステップ 1 ― 汎用ラップクラス Ref<T> の実装

Dart
class Ref<T> { T value; Ref(this.value); }
  • T は任意の型を表すジェネリクスです。intString、さらにはカスタムクラスでも使用できます。
  • コンストラクタで初期値を受け取り、value フィールドを公開しています。

使用例

Dart
void increment(Ref<int> counter) { counter.value += 1; // 直接参照を更新 }

この形にすれば、increment 関数は呼び出し元の counter オブジェクトを直接操作でき、結果が即座に反映されます。

ステップ 2 ― ValueNotifier で UI と連動させる

Ref<T> に加えて ValueNotifier を組み合わせると、Flutter の ValueListenableBuilder によるリアクティブ更新が可能です。

Dart
final ValueNotifier<Ref<int>> counter = ValueNotifier(Ref(0)); class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('参照渡しカウンタ')), body: Center( child: ValueListenableBuilder<Ref<int>>( valueListenable: counter, builder: (context, ref, _) { return Text( 'カウント: ${ref.value}', style: const TextStyle(fontSize: 24), ); }, ), ), floatingActionButton: FloatingActionButton( onPressed: () { // 参照渡しで更新 refIncrement(counter.value); // notifyListeners を手動で呼ぶ必要はなし → ref の中身が変化しただけで // ValueNotifier は同一オブジェクトなのでリビルドされない。 // そこで、ValueNotifier の `value = value;` で再通知。 counter.value = counter.value; // 再代入で通知トリガー }, child: const Icon(Icons.add), ), ); } } // 参照渡し関数 void refIncrement(Ref<int> r) { r.value += 1; }

ポイント解説

  • ValueNotifier<Ref<int>> は「Ref オブジェクト全体」を監視します。Ref の内部だけが変わっても ValueNotifier は自動で通知しません。そこで、counter.value = counter.value; のように同一インスタンスを再代入して通知を促すテクニックがあります。
  • もっとシンプルにしたい場合は ValueNotifier<int> を直接使うことも可能ですが、複数の異なる型を同一の API で扱いたいときに Ref<T> が威力を発揮します。

ステップ 3 ― 文字列を参照渡しで扱う例

String は immutable なので、ラップしないと変更できません。StringBuffer は可変オブジェクトですが、単純な置き換えの場合は以下のように Ref<String> を活用します。

Dart
void appendHello(Ref<String> text) { text.value = '${text.value} Hello'; } // UI 側 final ValueNotifier<Ref<String>> message = ValueNotifier(Ref('Welcome')); ... FloatingActionButton( onPressed: () { appendHello(message.value); message.value = message.value; // 再通知 }, child: const Icon(Icons.edit), );

ハマった点やエラー解決

項目 内容 解決策
ValueNotifier が再ビルドしない Ref の内部だけを変更しても ValueNotifier が通知しない 同一オブジェクトを再代入 (notifier.value = notifier.value;) または notifyListeners() を手動呼び出す
Ref を過度に使うとコードが冗長 ラップが多くなると可読性が低下 小規模なケースは ValueNotifier<T> を直接使用し、汎用ラップはライブラリ化してインポートする
String の結合でメモリが増える String は不変のため毎回新しいオブジェクトが生成される StringBuffer を内部的に使うラップクラス (Ref<StringBuffer>) を作成し、toString() で取得する
デバッグ時に値が更新されない debugPrint が古い値を表示 ValueListenableBuilderbuilder 内で print(ref.value); を入れて確認

解決策まとめ

  • 再通知: notifier.value = notifier.value; または notifier.notifyListeners();
  • 型安全: ジェネリクス Ref<T> で任意の型をラップし、関数シグネチャを統一
  • 文字列の効率: StringBuffer をラップするか、Ref<String> で置き換え操作を行う
  • 可読性向上: 小規模なら ValueNotifier<T>、大規模なら独自ラップクラスを utils.dart に切り出す

まとめ

本記事では、Flutter/Dart において プリミティブ型を参照渡し的に扱うテクニック を以下の三点に絞って解説しました。

  • 汎用ラップクラス Ref<T> を導入し、関数呼び出し時に実体の参照を共有できるようにしたこと。
  • ValueNotifierValueListenableBuilder を組み合わせ、UI に自動的に変更を反映させる実装例。
  • 文字列・数値それぞれの注意点 と、ハマりやすいポイントへの対処法(再通知、メモリ効率、可読性向上)を提示したこと。

これらを活用すれば、状態管理がシンプルになり、「呼び出し元の変数を直接更新できる」感覚でコードを書けるようになります。次回は ProviderRiverpod といったフレームワークと組み合わせた、よりスケーラブルな参照渡しパターンについて紹介する予定です。


参考資料