導入
Flutterは、シンプルで美しいユーザーインターフェースを構築するための強力なフレームワークです。
しかし、複雑なアプリケーションを開発する際には、コードの整理や状態管理が課題となります。
今回は、riverpod
とriverpod_annotation
を用いたMVVM (Model-View-ViewModel) パターンについて解説します。
MVVMパターンの概要
MVVMは、UIコードを整理し、コードの再利用性とテストの容易さを向上させるためのデザインパターンです。
以下の3つの主要コンポーネントから成り立っています。
- Model: アプリケーションのビジネスロジックやデータを管理
- View: ユーザーインターフェース (UI) のレンダリング
- ViewModel: ModelとViewの間を仲介し、データの取得やUIへの反映を担当
コンポーネントの関係
+--------+ +-----------+ +-------+
| View | <-----> | ViewModel | <-----> | Model |
+--------+ +-----------+ +-------+
MVVMの特徴
- 疎結合: ViewとModelは直接やり取りしません。ViewModelを通じて間接的にやり取りします。
- テスト容易性: ビジネスロジックやデータロジックがViewModelやModelに集中しているため、UIをテストせずにロジックを単体テストできます。
- 再利用性: ViewModelを再利用することで、異なるViewで同じロジックを利用できます。
Riverpodについて
FlutterのRiverpodは、状態管理と依存関係注入のためのパッケージです。Providerパッケージの進化形で、より柔軟で安全な設計になっています。
今回はflutter_riverpod
とriverpod_annotation
の利用方法について触れていきます。
依存関係の追加
まず、pubspec.yaml
に依存関係を追加します。
もしくは、pub add
コマンドで、以下のpubspec.yaml
に記載されたライブラリを追加します。
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.5.1
riverpod_annotation: ^2.3.5
dev_dependencies:
build_runner: ^2.4.11
riverpod_generator: ^2.4.2
各ライブラリをインストールする際には、最新のバージョンを以下から確認してください。
flutter_riverpod
riverpod_annotation
build_runner
riverpod_generator
コード生成の設定(任意)
build.yaml
に設定を追加し、生成されるファイルの出力先や命名規則をカスタマイズできます。
targets:
$default:
builders:
riverpod_generator:
options:
output_dir: lib/generated
flutter_riverpodの基本的な使い方
プロバイダーの定義
プロバイダーは、アプリ全体で共有したい値や状態を管理するためのものです。
以下の例では、カウンターの値を管理する StateProvider
を定義しています。
MVVMにおけるViewModelに当たる部分になります。
※今回は基本的にModelは登場しません。
import 'package:flutter_riverpod/flutter_riverpod.dart';
class Counter extends StateNotifier<int> {
Counter() : super(0);
void increment() {
state++;
}
void decrement() {
state--;
}
}
final counterProvider = StateNotifierProvider.autoDispose<Counter, int>((ref) {
return Counter();
});
ここで、autoDispose
というのはプロバイダーを適切に破棄してもらえるように設定するものになります。
プロバイダーの使用
ProviderScope
でラップすることによって、プロバイダーをアプリ全体で使用可能にします。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
// [重要]
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CounterScreen(),
);
}
}
Widgetでのプロバイダーの利用
上記のコード内に実装されているCounterScreenの中でcounterProviderを利用します。
プロバイダーの値を取得し、UIに反映させるには、StatelessWidget
ではなく、ConsumerWidget
を使います。
また、StatefulWidget
に対応するConsumerStatefulWidget
というものも存在します。
このCounterScreenはMVVMのViewに当たる部分になります。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class CounterScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// プロバイダーから値を取得
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(
title: Text('Counter'),
),
body: Center(
child: Text('Count: $count'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// プロバイダーの値を更新
ref.read(counterProvider.notifier).increment();
},
child: Icon(Icons.add),
),
);
}
}
以下のコードを利用することで、View(Widget)では、UIに関わる部分を、ViewModel(Provider/riverpod)では、値や状態の管理と、責任を分離することができます。
Riverpodのwatch/read/listen
ref.watch
、ref.read
、ref.listen
は、Riverpodでプロバイダーの状態を操作したり、監視したりするためのメソッドです。
これらは頻繁に利用する重要なメソッドなので、それぞれの違いや具体的な使用例について解説します。
⚫︎ ref.watch
プロバイダーの状態をリアクティブに監視し、プロバイダーの状態が変化すると自動的にUIを再描画します。これは、状態をUIにバインドするために使用します。
上にあるCounterScreen.dart
の例でも、buildメソッドの中ですぐに呼ばれています。
ref.watch(counterProvider)
はcounterProvider
の状態を監視し、countの値が変わるたびにUIを再描画します。
⚫︎ ref.read
ref.read
は、プロバイダーの現在の状態を1回だけ取得します。
これは、状態を読み取って即時に処理を行いたい場合に使用し、リアクティブに監視しません。
ref.read
は、基本的にボタンのコールバックなどで状態を変更したり、操作するために使用します。
上にあるCounterScreen.dart
の例では、FloatingActionButtonのonPressedメソッドの中で呼ばれています。
ref.read
をConsumerWidget
やConsumerState
のbuildメソッド内で使用すると、状態の変化に応じたUIの自動更新が行われず、期待通りに動作しません。
私はこの点を初め理解せず、readとwatchを混同して使っていましたが、注意が必要です。
UIが状態に応じて変化する場合や、プロバイダーの状態が変わるたびにUIを更新したい場合には、buildメソッド内でref.watch
を使用しましょう。
⚫︎ ref.listen
ref.listen
は、プロバイダーの状態の変化をトリガーして、状態が変化したときにコールバックを実行します。
以下に例を示します。
// プロバイダーの状態をリッスン
ref.listen<int>(
counterProvider,
(previousCount, newCount) {
if (newCount > 5) {
// countが5を超えたときにスナックバーによるアラートを表示
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('countが5を超えました!')),
);
}
},
);
ref.listen
の場合、状態が変わってもUIの再描画は行われませんが、上記の場合、countが5を超えると、スナックバーが表示される実装になっています。
リスナーが状態の変更を検知し、特定のロジックを実行する必要がある場合に使用します。
riverpod_annotation
の基本的な使い方
次に、riverpod_annotation
について解説します。
プロバイダーの定義
flutter_riverpod
と同様の状態管理用のカウンタープロバイダーを定義します。
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'counter_provider.g.dart';
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() {
state++;
}
void decrement() {
state--;
}
}
flutter_riverpod
の場合に比べると、かなり少ない実装で済むのが特徴です。
コード生成
ターミナルでbuild_runner
を実行して、プロバイダーのコードを自動生成します。
flutter pub run build_runner build
これにより、.g.dart
ファイルが生成されます。
使用方法
使い方は、正直flutter_riverpod
と同じであるので、割愛します。
参考(他のProvider)
StateNotifierProvider
の他にも、以下のプロバイダーがあります:
-
FutureProvider
: 非同期のデータ取得を管理します。 -
StreamProvider
: ストリームのデータを管理します。
FutureProvider
の例
定義
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'data_provider.g.dart';
@riverpod
Future<String> fetchData(FetchDataRef ref) async {
// ダミーの非同期処理
// 本来はAPIからデータを取得するなどの処理が記述されます。
await Future.delayed(Duration(seconds: 2));
return 'FetchedData';
}
ここで、FetchDataRefとは、自動生成されるリファレンス型です。
利用
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'data_provider.dart'; // 自動生成されたファイルをインポート
class DataAsyncScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final dataAsyncValue = ref.watch(fetchDataProvider);
return Scaffold(
appBar: AppBar(
title: Text('Data'),
),
body: Center(
child: dataAsyncValue.when(
data: (d) => Text('Data: $d'),
loading: () => CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
),
),
);
}
}
when
メソッドの中で、非同期処理でデータ取得できたら、dataの部分の処理を、ロード中はloading、処理に失敗した場合はerrorの処理をそれぞれ実行するようになっています。
StreamProvider
の例
中身としてはFutureProvider
と大きな差はありません。
定義
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'counter_stream_provider.g.dart';
@riverpod
Stream<int> counterStream(CounterStreamRef ref) async* {
int count = 0;
while (true) {
await Future.delayed(Duration(seconds: 1));
yield count++;
}
}
利用
class CounterScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final counterStream = ref.watch(counterStreamProvider);
return Scaffold(
appBar: AppBar(
title: Text('StreamProvider Example'),
),
body: Center(
child: counterStream.when(
data: (count) => Text('Count: $count'),
loading: () => CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
),
),
);
}
}
when
メソッドの部分も扱い方はFutureProvider
と同様な形になります。
最後に
MVVMパターンは、UIのロジックを整理し、コードの再利用性とテストの容易さを向上させるデザインパターンです。
riverpod
(flutter_riverpod)とriverpod_annotation
を用いることで、プロバイダーの定義がシンプルになり、手動でコードを書く必要がなくなります。
これにより、コードのメンテナンス性と生産性が向上します。
是非これらのパッケージを利用し、かつMVVMを意識してもらえたらと思います!