最も簡単なカウントアプリで Riverpod ・ flutter_hooks ・ Freezed の使い方を学んでみた

#最も簡単なカウントアプリで Riverpod ・ flutter_hooks ・ Freezed の使い方を学んでみた

### 最近、Flutter界隈では、RiverpodとFlutter Hooksが話題ということで記事にしておきます。
flutterを学び始めてまだ日は浅いので、もっと良い実装の仕方があると思いますが、自分の学習にもなればと今回デフォルトで用意されているカウントアプリをStatefulからRiverpodを使用したコードでの実装に変更していきます。
「もっとこうした方が良いよ」というご指摘などあれば、嬉しいです！

まず初めに**Riverpod**とは何か全くわからない人のために、簡単に言えば**状態管理をより扱いやすくするためのパッケージ**です。
###状態管理とはなにか？
初心者がFlutterで開発する上で初めの方にまず疑問に思うことが「Stateless? Statefull? 何それ？」と思うはずです。(自分が経験しました)
ざっくりいうと、Statelessとは状態を持ちません。UIに固定で表示されるものです。
Statefullとは反対に状態を持ちます。
じゃあ状態を持つって何？　って初心者の方はなりますよね。それは、、、
データを元にUIを作る仕組みのことを状態を持つと呼びます。
その状態を扱いやすく管理することを状態管理といいます！

では本題に移ります。
今回アーキテクチャをMVVMパターンで設計しています。
まずパッケージのインストールを行います。


```pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

  flutter_hooks: <任意のバージョン>
  hooks_riverpod: <任意のバージョン>

  freezed_annotation: <任意のバージョン>

dev_dependencies:
  flutter_test:
    sdk: flutter

  build_runner:
  freezed: <任意のバージョン>
  json_serializable: <任意のバージョン>
```

次にデータクラスをFreezedを使い、作成します

```count_data.dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'count_data.freezed.dart';
part 'count_data.g.dart';

@freezed
class CountData with _$CountData {
  const factory CountData({
    required int count,　//カウントの総数
    required int countUp, //カウントアップ
    required int countDown, //　カウントダウン
  }) = _CountData;

  // {} ではなく =>（アロー）を使ってください
  factory CountData.fromJson(Map<String, dynamic> json) =>
      _$CountDataFromJson(json);
}
```

単純にカウントを増やすだけでなく、カウントを減らすボタンやリセットするボタンを実装しています。
次に、ViewとModelのやりとりの中間に位置するView_Modelの実装を行います。
view_modelの役割を果たす部分を**count_data_controller.dart**というファイルに記述していきます。
先程定義したデータクラスを元にStateNotifierを使用し、CountDataControllerというクラスを作成します。

```count_data_controller.dart
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:hooks_riverpod_freezed_countapp/model/count_data.dart';

class CountDataController extends StateNotifier<CountData> {
  CountDataController(this._reader)
      : super(CountData(
          count: 0,
          countUp: 0,
          countDown: 0,
        )); //初期値を全て0に設定しています

  final Reader _reader;

//カウントの総数とカウントアップの回数を１増やすメソッド
  void increment() {
    state = state.copyWith(
      count: state.count + 1,
      countUp: state.countUp + 1,
    );
  }

//カウントの総数を1減らしカウントダウンの回数を１増やすメソッド
  void decrement() {
    state = state.copyWith(
      count: state.count - 1,
      countDown: state.countDown + 1,
    );
  }

//全てのカウントをリセットするメソッド
  void reset() {
    state = CountData(count: 0, countUp: 0, countDown: 0);
  }
}

```

viewModelのクラスが作成できたら、次にProviderを記述していきます。
別でファイルを作成しても良いのですが、今回はCountDataControllerクラスがあるファイルに書いています！
StateNofitierProviderを使用して、countDataProviderを作成します。
**1個目の型パラメータにはStateNotifierの型を、2個目の型パラメータにはStateNotifierが保持する型を持たせる必要があります。**
stateの値が変わると自動的に変更の通知をしてくれます！
notifyListeners()をわざわざ書かなくて済むので書き忘れがなくなり便利です！

```count_data_controller.dart
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:hooks_riverpod_freezed_countapp/model/count_data.dart';

final countDataProvider = StateNotifierProvider<CountDataController, CountData>(
  (ref) => CountDataController(ref.read),
);

<省略>
```

これでModelとViewModelは以上です！
あとはUIに位置するViewの部分の実装をして完成になります！
以前は、 **HookWidget と useProvider** の組み合わせにより、各Providerを読み取ることができていましたが、Riverpod v1.0.0からは**HookConsumerWidgetとref.watch(read)**を使用して書いていきます。
**HookConsumerWidget**を継承することで、buildメソッドの引数に**WidgetRef ref**が追加されます。
ref.watch(Provider名)とすることで、そのProviderにアクセスすることができます。watchを使う場合、値の変更があった時にリビルドされ画面が更新されます。
readを使う場合は、リビルドはされません。(`例えば、カウントアプリでプラスボタンを押したときにリビルドされて欲しいのは数字のみで押したボタンのリビルドは必要ないです`)

:::note warn
 ref.read に渡す Provider は、.notifier を付与する点に注意しましょう。
:::


```count_page.dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:hooks_riverpod_freezed_countapp/view_models/controller/count_data_controller.dart';

class MyHomePage extends HookConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) { //追加
    final countState = ref.watch(countDataProvider);
    final countNotifier = ref.read(countDataProvider.notifier);

    return Scaffold(
      appBar: AppBar(
        title: Text(
          ref.watch(titleProvider),
        ),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              countState.count.toString(),
              style: Theme.of(context).textTheme.headline4,
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [
                FloatingActionButton(
                  onPressed: () {
                    countNotifier.decrement();
                  },
                  tooltip: 'Increment',
                  child: const Icon(Icons.horizontal_rule),
                ),
                FloatingActionButton(
                  onPressed: () {
                    countNotifier.increment();
                  },
                  tooltip: 'Increment',
                  child: const Icon(Icons.add),
                ),
              ],
            ),
            Padding(
              padding: const EdgeInsets.only(top: 8.0),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  countState.countDown.toString()),
                  countState.countUp.toString()),
                ],
              ),
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          countNotifier.reset();
        },
        tooltip: 'Increment',
        child: const Icon(Icons.restart_alt),
      ),
    );
  }
}

```

以上で、カウントアプリの実装は完了になります。
今回参考にさせて頂いた記事は以下になります。

https://zenn.dev/riscait/books/flutter-riverpod-practical-introduction/viewer/migrate-to-v1

https://note.com/mxiskw/n/n5c06bc2dd0d5