【Flutter】Signals x Riverpod x MVVM という新しい設計手法を考えてみた (ヘルパー編)
前回作ったSignalsとRiverpodによるMVVMをもう少し改良します。
ヘルパーとは
前編では Flutter Hooks のsetState
の代わりにSignalsのsignal
を使うと、Widgetを分割しやすくなったりロジックをViewModelに移せるようになったりして便利という話をしました。
ただ、設計にこだわりのある方なら、ボタンをタップした時に実行する
vm.counter.value += 1
が微妙だな…と感じるかと思います。
これはRiverpod3.0からStateProvider
が非推奨になり、代わりにNotifierProvider
を使用するようになった理由と関連してくるのですが、
「値の更新方法を限定するとコードが管理しやすくなり、不具合も起きにくくなる」という設計のテクニックがあります。
例えば、今後アプリに機能が増えて、色々なところで+=1
という処理を書くかもしれません。
カウンターの個数を増やそう…とか、スマホのカメラに手をかざすだけでも+=1
する機能を付けよう…などなど。
その後、
「1ずつしか増やせないのは止めて、ユーザーが自由に増やす値を設定できるようにしよう」となった際に、その+=1
と書いた箇所を全部修正する必要があります。
面倒ですし、一箇所でも修正漏れがあれば不具合になってしまいますね。
なので、+=1
と書く代わりにincrement
というメソッドを作り、それでしか更新できないようにすれば、そのincrement
内の処理を変更するだけで対応完了になります。
このように、同じ処理を二回以上書く場合に、処理部分を一つのメソッドとしてまとめる手法をヘルパー(Helper)と言い、メソッド自体はヘルパー関数と呼びます。
ヘルパー関数を追加する
では、前編のシンプルな方のカウンターアプリを改良していきます。
signal
にはヘルパーを考慮してかreadonly()
というメソッドが用意されています。
これを最初のカウンターアプリに使用してみます。
class CounterViewModel {
- final counter = signal(0);
+ final counter = signal(0).readonly();
CounterViewModel() {
effect(() {
print("$counter");
});
}
}
これで、counter
がSignal
型からReadonlySignal
型になり、build
メソッド内でcounter.value += 1
と書けなくなりました。
次に、NotifierProvider
側にincrement
メソッドを追加します。
class CounterNotifier extends _$CounterNotifier {
CounterViewModel build() => CounterViewModel();
+ void increment() => (state.counter as Signal).value += 1;
}
実はReadonlySignal
型はSignal
型の親クラスなので、こうしてSignal
型に戻してやるだけで再び更新できます。
とはいえ毎回as Signal
と書くのも面倒なのでextension
を作っておきます。
extension Writable<T> on ReadonlySignal<T> {
T get writable => value;
set writable(T value) => (this as Signal<T>).value = value;
}
void increment() => state.counter.writable += 1;
さて、これでvalue
からは直接更新が出来ないようになりました。
build
メソッド内で更新する方法はRiverpodのNotifierProvider
を更新する時と同じです。
ref.read(counterNotifierProvider.norifier).increment();
ただ、これだとちょっと長いですよね。それにvm
経由で更新できた方が分かりやすいです。
ということでCounterViewModel
にnorifier
を追加します。
class CounterNotifier extends _$CounterNotifier {
- CounterViewModel build() => CounterViewModel();
+ CounterViewModel build() => CounterViewModel(counterNotifierProvider.notifier);
void increment() => state.counter.writable.value += 1;
}
class CounterViewModel {
final counter = signal(0);
+ final Refreshable<CounterNotifier> notifier;
- CounterViewModel() {
+ CounterViewModel(this.notifier) {
effect(() {
print(counter);
});
}
}
これでvm.notifier
と書けるようになりました。
ref.read(vm.norifier).increment();
最後にbuild
メソッド内の処理をincrement
に置き換えます。
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.watch(counterNotifierProvider);
return Scaffold(
body: Center(
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Watch((context) => Text('counter: ${vm.counter}')),
- ElevatedButton(onPressed: () => vm.counter.value += 1, child: const Text("+1")),
+ ElevatedButton(onPressed: ref.read(vm.norifier).increment, child: const Text("+1")),
]),
),
);
}
おわりに
Signalsには今回ご紹介しきれないくらい沢山の機能があります。
国内でもSignalsの使い方についての議論が深まっていくと嬉しいです。
備考
本記事は以下の環境で検証しました
flutter_riverpod: 2.5.1
riverpod_annotation: 2.3.5
riverpod_generator: 2.4.3
signals: 5.5.0
Flutter 3.24.1 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 5874a72aa4 (13 days ago) • 2024-08-20 16:46:00 -0500
Engine • revision c9b9d5780d
Tools • Dart 3.5.1 • DevTools 2.37.2
Discussion