Flutter 🇯🇵
Published in

Flutter 🇯🇵

Flutter/Dartにおけるimmutableの実践的な扱い方

Photo by JJ Ying on Unsplash

mutableなクラスの挙動

class Mutable {
Mutable(this.value);
int value;
}
void main() {
test('mutable', () {
final x1 = Mutable(1);
final x2 = x1;
x1.value++;
expect(x1.value, 2);
// x2とx1の参照先は同一なのでx1のvalueが変更されるとm2も変更される
expect(x2.value, 2);
expect(x1 == x2, isTrue);
expect(identical(x1, x2), isTrue);
});
}

immutableなクラスの挙動

class Immutable {
Immutable(this.value);
final int value;
}
void main() {
test('immutable', () {
var x1 = Immutable(1);
final x2 = x1;
// 後から変更不可能(=immutable)
// x1.value++;
// immutableなオブジェクトは変更操作ができず
// 違う値を持たせたい場合は再生成が必要
x1 = Immutable(x1.value + 1);
expect(x1.value, 2);
// x1を変更したわけではなく再生成したのでx2のvalueは元のまま
expect(x2.value, 1);
// 参照も違う
expect(identical(x1, x2), isFalse);
});
}

@immutableアノテーションの活用

// @immutableアノテーション付ける
@immutable
class Immutable2 {
// 全てのフィールドがfinalだと生成後に不変であることが保証されるのでconstにできる。
// (@immutableにより、constを付けないと警告がでて指定漏れを教えてくれる)
const Immutable2(this.value);
// finalが欠けるとimmutableとして成り立たないのでコンパイルエラー
final int value;
}
  • immutableなクラスとしての要件を満たしていない時に警告が出る
  • コンストラクターに const の付与が促される(漏れていると警告が出る)
  • クラス利用者に、それがimmutableなクラスであることが一目で伝わる

const とは?

void main() {
test('immutable const', () {
final x1 = Immutable2(1);
final x2 = Immutable2(1);
expect(identical(x1, x2), isFalse);
const x3 = Immutable2(1);
const x4 = Immutable2(1);
expect(identical(x3, x4), isTrue);
});
}
void main() {
test('const list', () {
final x1 = <int>[];
x1.add(1);
// Unsupported operation: Cannot add to an unmodifiable list
const x2 = <int>[];
x2.add(1);
});
}

immutableなクラスの実装はfreezed を使うと楽

import 'package:freezed_annotation/freezed_annotation.dart';part 'test.freezed.dart';@freezed
class Immutable3 with _$Immutable3 {
const factory Immutable3(int value) = _Immutable3;
}
void main() {
test('immutable freezed', () {
var x1 = const Immutable3(1);
final x2 = x1;
// freezedを使うと、特定のフィールドを更新しつつ
// それ以外のフィールドの値は維持された
// 新しいオブジェクトとしてコピーするメソッドを自動生成してくれる
// (手動でももちろんそういうメソッドを定義すれば済むが、
// 自動生成だと楽かつ実装ミスでのバグの心配も不要)
x1 = x1.copyWith(value: x1.value + 1);
expect(x1.value, 2);
expect(x2.value, 1);
expect(identical(x1, x2), isFalse);
});
}
  • factoryコンストラクターの定義だけで済んでいる(フィールド宣言を手動で書く必要がない)
  • copyWith メソッドの自動生成
https://pub.dev/packages/freezed#deep-copy

StateNotifierはimmutableなstate値であることが前提となっている

class Mutable {
Mutable(this.value);
int value;
}
class _Controller extends StateNotifier<Mutable> {
_Controller() : super(Mutable(0));
void increment() {
state.value++;
// 無理矢理以下のように書くと一応動く
// state = state;
}
}
class _Home extends ConsumerWidget {
const _Home({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final controller = ref.watch(_controller.notifier);
// stateのvalueが変わってもリビルドが起こらない
   final state = ref.watch(_controller);
return Scaffold(
appBar: AppBar(),
body: Center(
child: ElevatedButton(
onPressed: controller.increment,
child: Text('${state.value}'),
),
),
);
}
}
final state = ref.watch(_controller.select((s) => s));
final state = ref.watch(_controller.select((s) => s.value));

コレクションの扱い

void main() {
test('list mutable', () {
final list1 = [Mutable(1), Mutable(2)];
final list2 = list1;
list1[0] = Mutable(3);
// list1の変更にlist2が完全に巻き込まれる
print(list1); // [value: 3, value: 2]
print(list2); // [value: 3, value: 2]
expect(list1 == list2, isTrue);
});
}

シャローコピー

void main() {
test('list mutable shallow copy', () {
var list1 = [Mutable(1), Mutable(2)];
final list2 = list1;
list1 = List.of(list1);
list1[0] = Mutable(3);
// list1はシャローコピーされてから変更されたため、
// list2はその影響を受けない
print(list1); // [value: 3, value: 2]
print(list2); // [value: 1, value: 2]
expect(list1 == list2, isFalse);
});
}
void main() {
test('list mutable shallow copy broken', () {
var list1 = [Mutable(1), Mutable(2)];
final list2 = list1;
list1 = List.of(list1);
list1[0] = Mutable(3);
list1[1].value = 4;
// シャローコピー後でも、mutableな同一インスタンスの
// 変更操作をされるとその影響を受けてしまう
print(list1); // [value: 3, value: 4]
print(list2); // [value: 1, value: 4]
expect(list1 == list2, isFalse);
});
}

コレクションの要素がimmutableならシャローコピーだけで充分

void main() {
test('list mutable shallow copy broken', () {
var list1 = const [Immutable3(1), Immutable3(2)];
final list2 = list1;
list1 = List.of(list1);
list1[0] = const Immutable3(3);
// mutate操作は不可能
// list1[1].value = 4;
// list2自体に触れずにその変更をすることは不可能
print(list1); // [value: 3, value: 4]
print(list2); // [value: 1, value: 2]
expect(list1 == list2, isFalse);
});
}
void main() {
test('list immutable shallow copy borken', () {
var list1 = [
[Immutable3(1)],
[Immutable3(2)]
];
final list2 = list1;
list1 = List.of(list1);
list1[0].clear();
// list2自体に触れずにその変更できてしまう
print(list1); // [[], [value: 2]]
print(list2); // [[], [value: 2]]
expect(list1 == list2, isFalse);
});
}
@freezed
class Immutable4 with _$Immutable4 {
const factory Immutable4(List<int> values) = _Immutable4;
}
void main() {
test('Immutable mutable list', () {
final x1 = Immutable4([1, 2]);
final x2 = x1;
x1.values.clear();
print(x1); // values: []
print(x2); // values: []
});
}
void main() {
test('lImmutable mutable unmodified list', () {
final x1 = Immutable4(UnmodifiableListView([1, 2]));
final x2 = x1;
// 実行時エラー(Unsupported operation: Cannot clear an unmodifiable list)
x1.values.clear();
});
}
@freezed
class Immutable4 with _$Immutable4 {
const factory Immutable4(List<int> values) = _Immutable4;
}
class FooNotifier extends StateNotifier<Immutable4> {
FooNotifier() : super(const Immutable4([]));
void add(int value) {
state = state.copyWith(
// 2. 外から `state.values`に対して直接mutate操作されることも防ぐ
values: UnmodifiableListView(
// 1. シャローコピーで元のvaluesと相互に変更の影響を受けないようにする
[
...state.values,
value,
],
),
);
}
}

実際には意図しないコレクションのmutate操作をしにくい

final _controller = StateNotifierProvider<_Controller, Immutable4>(
(_) => _Controller(),
);
@freezed
class Immutable4 with _$Immutable4 {
const factory Immutable4(List<int> values) = _Immutable4;
}
class _Controller extends StateNotifier<Immutable4> {
_Controller() : super(Immutable4([]));
void f() {
state.values.add(1);
}
}
void f() {
state.values.add(1);
state = state;
}
// `f()`実行後、buildメソッドが呼ばれる
final state = ref.watch(_controller);
final state = ref.watch(_controller.select((s) => s.values));

コレクション系クラスのimmutable操作紹介

  • .of() メソッドでシャローコピーしてからmutate操作( .. (cascade notation)との組み合わせることが多い)
  • ... (スプレッド演算子)で展開しつつ(この時シャローコピーされる)、追加・上書きしたい値を添える

そもそもFlutterでのアプリ開発において、immutableプログラミングは必要か?

https://github.com/flutter/flutter/blob/4a939d7a57427f74763bb0107dec4ddf3c75f1df/packages/flutter/lib/src/material/theme.dart#L156
https://github.com/flutter/flutter/blob/b8a2456737c9645e5f3d7210fba6267f7408486f/packages/flutter/lib/src/material/theme_data.dart#L197
https://github.com/flutter/flutter/blob/b8a2456737c9645e5f3d7210fba6267f7408486f/packages/flutter/lib/src/material/theme_data.dart#L1326
  • Struct・data classのようなimmutableプログラミングに適した入れ物が無い
  • 標準コレクションクラスはmutableなインターフェースのものしかない
  1. immutableクラスの実装はfreezed 活用 (手動で書くのが怠くミスする可能性もある copyWith の自動実装メリットが大きい)
  2. immutableクラスの利用を徹底した前提で、コレクション系は標準クラスを使いつつimmutalbe操作はシャローコピーでOK(意図せぬ外部変更が気になるなら、さらにUnmodifiableXxxViewで守っておく)

--

--

Flutterに関する日本語記事を書いていきます🇯🇵

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store