概要
Flutterで riverpod という状態管理ライブラリを使って、MVVMアーキテクチャにおけるViewModelを作成する。 riverpodは、v2になり色々と作成方法も変わったので、基本v2でアノテーションによるコード生成を使う前提で記載。
使用するライブラリ
freezedはViewModelがViewに対して公開する状態を管理するために使用。 (Stateはimmutableであることが好ましいため。)
Flutterで新規プロジェクト作成時のカウンターアプリ
簡単なサンプルとして、Flutterのプロジェクト作成時に生成されているカウンターアプリの機能をViewModelを使って実装する。
[初期状態のコード]
class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headlineMedium, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); } }
ディレクトリ構成
lib/ ├── main.dart └── widget/ ├── app.dart └── pages/ └── home/ ├── home_page.dart ├── view_model.dart └── ui_state.dart
pages/ ディレクトリを作成し、各ページごとに一つのディレクトを作成する。
※ view_model.dart, ui_state.dartについては、コード生成により、生成される <file名>.g.dart
も一緒に作成される。
状態ファイルの作成
import 'package:freezed_annotation/freezed_annotation.dart'; part 'ui_state.freezed.dart'; @freezed class UiState with _$UiState { const factory UiState({ required int counter, }) = _UiState; }
Viewに公開したい情報は、インクリメントされるカウンターの数値のためこれをプロパティとして持つUiStateというクラスを作成する。
freezedによりコード生成するため、@freezed
アノテーションと ui_state.freezed.dart
というファイルを part で読み込む必要がある。
dart run build_runner build
というコマンド実行で、freezedの生成ファイル(ui_state.freezed.dart
)が生成される。
ViewModelファイルの作成
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'ui_state.dart'; part 'view_model.g.dart'; @riverpod final class ViewModel extends _$ViewModel { @override UiState build() { return const UiState( counter: 0, ); } }
RiverpodでViewModelを作るときは、NotifierProviderを使う。
NotifierProviderを自動生成するときは、build
というメソッドをoverrideして、ここで先程のUIStateを初期化する。
freezedと同様にこちらも、コード生成するため、 @riverpod
、part 'view_model.g.dart';
を記載して、
dart run build_runner build
でコード生成する。
ViewModelファイルに incrementCounter
メソッドを追加する
@riverpod final class ViewModel extends _$ViewModel { @override UiState build() { return const UiState( counter: 0, ); } void incrementCounter() { state = state.copyWith(counter: state.counter + 1); } }
ViewからViewModelに対して、カウンター増加を通知するためのメソッドを追加する。
stateプロパティから、現在管理しているUiStateインスタンスにアクセスでき更新が可能。
freezedは、一部のプロパティを更新した新しいインスタンスを copyWith
というメソッドをもっていて、これを使うことで、counterを更新した新しいUiStateのインスタンスを生成して、stateを更新することができる。
state = state.copyWith(counter: state.counter + 1);
ViewModelを利用するPageウィジェットの完成コード
class HomePage extends ConsumerWidget { const HomePage({super.key, required this.title}); final String title; @override Widget build(BuildContext context, WidgetRef ref) { final viewModel = ref.read(viewModelProvider.notifier); final counter = ref.watch(viewModelProvider); return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'You have pushed the button this many times:', ), Text( '$counter', style: Theme.of(context).textTheme.headlineMedium, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: viewModel.incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), ); } }
Riverpodを利用するウィジェットは ConsumerWidget
を継承する必要がある。
NotifierProviderからViewModelを参照する場合、 viewModelProvider.notifier
のようにnotifierをreadすればよい。(viewModelは変更を受け取る必要がないため、ref.readを使う)
NotifierProviderからUiStateを参照する場合、viewModelProviderをref.watchを使って参照する。
(UiStateは変更を検知する必要があるため、watchを使う)