Riverpod v2を使用してページとページの状態を１対１で紐つけて簡単に管理する【Flutter】

# はじめに
今回は、Flutterで簡単に状態管理をする手法について、備忘録的に書いておく記事です。
内容が間違っている場合、もっといいやり方がある場合はコメントにてお願いします。

## 今回の環境
- Flutter 3.10.1
- flutter_riverpod 2.4.10
- riverpod_generator 2.3.11
- riverpod_annotation 2.3.4
- freezed 2.5.2
- freezed_annotation 2.4.1
- build_runner　2.4.9

### 今回の記事でやること
以下の図のような状態管理をします。

![無題の図形描画 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778030/5470ab87-d5bd-75cf-df32-752e39fd947f.png)

viewとstateが１対１(または1対多)で紐ついています。

controllerはviewModelを言い換えても良いです。modelから作られたインスタンスの状態を返したり、インスタンスの状態を変更するメソッドもcontrollerが持ちます。

### 実際のコード

#### viewのコード

```dart:test.dart
import 'package:advanced_search_2/ui/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class TestPage extends StatelessWidget {
  const TestPage({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: "Riverpod"),
    );
  }
}

class MyHomePage extends ConsumerWidget { // riverpodを使うためConsumerWidget
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    //  ↓ contollerの状態やメソッドを呼び出すためのコード ↓
    final counter =
        ref.watch(testPageStateProvider.select((value) => value.counter));

    final incrementCounter =
        ref.read(testPageStateProvider.notifier).incrementCounter;

    final decrementCounter =
        ref.read(testPageStateProvider.notifier).decrementCounter;

    final doubleCounter =
        ref.read(testPageStateProvider.notifier).doubleCounter;

    final clearCounter = ref.read(testPageStateProvider.notifier).clearCounter;

    //  ↑ contollerの状態やメソッドを呼び出すためのコード ↑
    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,
            ),
            const SizedBox(height: 20),
            ElevatedButton(
                onPressed: () {
                  incrementCounter();
                },
                child: const Text("increment")),
            ElevatedButton(
                onPressed: () => decrementCounter(),
                child: const Text("decrement")),
            ElevatedButton(
                onPressed: () => doubleCounter(), child: const Text("double")),
            ElevatedButton(
                onPressed: () => clearCounter(), child: const Text("clear")),
          ],
        ),
      ),
    );
  }
}
```
#### controller・modelのコード
```dart:state.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'state.freezed.dart';
part 'state.g.dart';

// 管理する状態とその初期値を書きます
@freezed
class TestState with _$TestState {
  const factory TestState({
    required int counter,
    required String title,
    required String message,
    String? description,
  }) = _TestState;
}

@riverpod
final class TestPageState extends _$TestPageState {
  @override
  TestState build() {
    init();
    return const TestState(
      counter: 0,
      title: "RIVERPOD",
      message: 'You have pushed the button this many times:',
    );
  }

  // 状態を更新するメソッドを書きます
  void incrementCounter() {
    state = state.copyWith(counter: state.counter + 1);
  }

  void decrementCounter() {
    state = state.copyWith(counter: state.counter - 1);
  }

  void doubleCounter() {
    state = state.copyWith(counter: state.counter * 2);
  }

  void clearCounter() {
    state = state.copyWith(counter: 0);
  }
}
```

:::note warn
`dart run build_runner build`の実行を忘れずに！
:::

以上のコードで基本的な状態管理は可能です。
次はオプション的な記述を紹介します。

### `.autoDispose`を無効化
riverpod v2 以降では、autoDisposeがデフォルトで付与されています。
これを無効化(コントローラーの状態を破棄させない)するためには、明示的に書いてあげる必要があります。

```diff_dart
- @riverpod
+ @Riverpod(keepAlive: true)
final class TestPageState extends _$TestPageState {
```
こうすることで、ページ遷移などで状態が破棄されることはなくなります。

### Provider生成時に処理を実行する
今回の例で言うと、counterの値をmodelの初期値と違う値にしたい場合。
以下のような書き方をすることで、Provider生成時に処理を実行することができます。

```dart
final class TestPageState extends _$TestPageState {
  @override
  TestState build() {
    state = const TestState(
      counter: 0,
      title: "RIVERPOD",
      message: 'You have pushed the button this many times:',
    );
    init();
    return state;
  }

  void init() {
    state = state.copyWith(counter: 5);
  }
// ~~~~~~ 省略 ~~~~~~
```

こうすることで、stateを`init()`で書き換えた、`counter: ５`を初期値とすることができます。
非同期の処理を挟みたい場合は`AsyncNotifierProvider`を使用する必要があります(多分)。

### おわりに
お読みいただきありがとうございました！
Riverpodはv1しか触ってこなかったので、違和感がすごいですね...
riverpodを使用した状態管理はググるのがむずかしいです。（v1,v2やannotation、fooksの使用不使用などなど...）
[riverpodのGitHub](https://github.com/rrousselGit/riverpod)でissueやDiscussionsを掘るのが正解な気がします。