前回この記事でRiverpodを使ったMVUアーキテクチャについて書いた。
が、いくつか問題があるなと気づいた。
- UIに関する処理を書く場所がない
- 複数のStateを同時に扱う場合が考慮されていない(ビジネスロジックを書く場所がない)
なので、ViewModelを挟むのがよいのではと考えた。
// lib/screens/home/_home_view_model.dart import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../providers/providers.dart'; final viewModelProvider = Provider((ref) => HomeViewModel(ref.read)); final navigationProvider = StateProvider((ref) => NavigationState.none); /// 値を持たせたい場合は、freezedなどを使う enum NavigationState { none, showSnackbar, } class HomeViewModel { HomeViewModel(this._read); final Reader _read; Future<void> increment() async { final countValue = _read(counterNotifierProvider); final count = countValue.data?.value; if (count == null) { return; } final newCount = count + 1; /// LocalStorageのデータも新しいデータで更新 final sharedPreference = await _read(sharedPreferenceProvider.future); await sharedPreference.setInt('counter', newCount); /// counter以外、他のStateを更新しても問題ない /// 処理を行ったらStateの更新 /// CounterNotifierのupdateを呼び出してStateを更新 await _read(counterNotifierProvider.notifier).update(newCount); /// 更新後にSnackbarを表示する _read(navigationProvider).state = NavigationState.showSnackbar; } }
// lib/screens/home/home_screen.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../providers/providers.dart'; import '_home_view_model.dart'; class Home extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { ref.listen<StateController<NavigationState>>(navigationProvider, (value) { switch (value.state) { case NavigationState.showSnackbar: ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('更新されました')), ); break; default: break; } }); return Scaffold( appBar: AppBar(title: const Text('Counter example')), body: Center( child: Consumer(builder: (context, ref, _) { /// 初期値の読み込みやStateが変更されたらrebuildなどwatchすれば行われる final count = ref.watch(counterNotifierProvider); /// 非同期でカウント数を読み込むので、loading, errorなどをちゃんと表現する return count.map( data: (data) => Text('${data.value}'), loading: (_) => const CircularProgressIndicator(), error: (_) => const Text('On Error'), ); }), ), floatingActionButton: FloatingActionButton( /// ここでStateを更新するのではなくViewModelを経由する onPressed: () => ref.read(viewModelProvider).increment(), child: const Icon(Icons.add), ), ); } }
本来ViewModelは、UIに関する処理のみを担ってビジネスロジックは別のクラスであるUseCaseなどが担うべきな気はするけど、毎回UseCaseを作るのはめんどくさいので気にしていない。ビジネスロジックが大きくなってきたり共通化したい場合にUseCaseに分けるなどすればよいと思う。