MVVM考えながら、Riverpod使ってみた

# 導入

Flutterは、シンプルで美しいユーザーインターフェースを構築するための強力なフレームワークです。
しかし、複雑なアプリケーションを開発する際には、コードの整理や状態管理が課題となります。

今回は、`riverpod`と`riverpod_annotation`を用いたMVVM (Model-View-ViewModel) パターンについて解説します。

# MVVMパターンの概要

MVVMは、UIコードを整理し、コードの再利用性とテストの容易さを向上させるためのデザインパターンです。
以下の3つの主要コンポーネントから成り立っています。

- **Model**: アプリケーションのビジネスロジックやデータを管理
- **View**: ユーザーインターフェース (UI) のレンダリング
- **ViewModel**: ModelとViewの間を仲介し、データの取得やUIへの反映を担当

## コンポーネントの関係

```plaintext
+--------+         +-----------+         +-------+
|  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`に記載されたライブラリを追加します。

```yaml: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](https://pub.dev/packages/flutter_riverpod)
[riverpod_annotation](https://pub.dev/packages/riverpod_annotation)
[build_runner](https://pub.dev/packages/build_runner)
[riverpod_generator](https://pub.dev/packages/riverpod_generator)

## コード生成の設定（任意）

`build.yaml`に設定を追加し、生成されるファイルの出力先や命名規則をカスタマイズできます。

```yaml:build.yaml
targets:
  $default:
    builders:
      riverpod_generator:
        options:
          output_dir: lib/generated
```

## flutter_riverpodの基本的な使い方

### プロバイダーの定義

プロバイダーは、アプリ全体で共有したい値や状態を管理するためのものです。
以下の例では、カウンターの値を管理する `StateProvider` を定義しています。
MVVMにおけるViewModelに当たる部分になります。
※今回は基本的にModelは登場しません。

```dart:CounterProvider.dart
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`でラップすることによって、プロバイダーをアプリ全体で使用可能にします。

```dart
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に当たる部分になります。

```dart: CounterScreen.dart
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メソッドの中ですぐに呼ばれています。

<font color='red'>`ref.watch(counterProvider)`は`counterProvider`の状態を監視し、countの**値が変わるたびにUIを再描画**します。</font>

### ⚫︎ `ref.read`
`ref.read`は、プロバイダーの現在の状態を**1回だけ取得**します。
これは、状態を読み取って即時に処理を行いたい場合に使用し、**リアクティブに監視しません。**
`ref.read`は、基本的にボタンのコールバックなどで状態を変更したり、操作するために使用します。

上にある`CounterScreen.dart`の例では、FloatingActionButtonのonPressedメソッドの中で呼ばれています。

:::note warn
`ref.read`を`ConsumerWidget`や`ConsumerState`のbuildメソッド内で使用すると、状態の変化に応じたUIの自動更新が行われず、期待通りに動作しません。
私はこの点を初め理解せず、readとwatchを混同して使っていましたが、注意が必要です。
**UIが状態に応じて変化する場合や、プロバイダーの状態が変わるたびにUIを更新したい場合には、buildメソッド内で`ref.watch`を使用しましょう。**
:::

### ⚫︎ `ref.listen`
`ref.listen`は、プロバイダーの状態の変化をトリガーして、**状態が変化したときにコールバックを実行**します。
以下に例を示します。

```dart
// プロバイダーの状態をリッスン
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`と同様の状態管理用のカウンタープロバイダーを定義します。

```dart
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`を実行して、プロバイダーのコードを自動生成します。

```bash
flutter pub run build_runner build
```

これにより、`.g.dart`ファイルが生成されます。

## 使用方法

使い方は、正直`flutter_riverpod`と同じであるので、割愛します。

## 参考(他のProvider)

`StateNotifierProvider`の他にも、以下のプロバイダーがあります：

- **`FutureProvider`**: 非同期のデータ取得を管理します。
- **`StreamProvider`**: ストリームのデータを管理します。

### `FutureProvider` の例

#### 定義
```dart
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とは、自動生成されるリファレンス型です。

#### 利用

```dart
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`と大きな差はありません。

#### 定義
```dart
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++;
  }
}

```

#### 利用

```dart
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を意識してもらえたらと思います！