Flutter: Riverpod(v2)を使ったViewModelの作り方
概要
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;
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
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/ ディレクトリを作成し、各ページごとに一つのディレクトを作成する。
- home_page.dart: 実際のページウィジェット(View)
- view_model.dart: ViewModel
- ui_state.dart: ViewModelがViewに公開する状態
※ view_model.dart, ui_state.dartについては、コード生成により、生成される <file名>.g.dart
も一緒に作成される。
状態ファイルの作成
import 'package:freezed_annotation/freezed_annotation.dart';
part 'ui_state.freezed.dart';
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';
final class ViewModel extends _$ViewModel {
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
でコード生成する。
incrementCounter
メソッドを追加する
ViewModelファイルに final class ViewModel extends _$ViewModel {
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;
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を使う)
Discussion