state_notifier と freezed を使って、Flutterのカウンターアプリをつくるよ

## はじめに

いま、Flutter界隈では、**「state_notifier + freezed」を使って開発するのがアツい!** と、評判です。

> Flutter state_notifierいい感じなので使ったほうが良いですよ
https://qiita.com/_masaokb/items/fe77495db0aeba226d2a
<img width="744" alt="スクリーンショット 2020-05-18 14.42.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93703/5a04458e-6c07-4617-0715-f040a9c78699.png">

「state_notifier」は、providerと組み合わせて使い、Widgetから**「状態」と「ロジック」を簡単に分離し&通知する**ことができるライブラリです。**Widgetの状態管理を楽にしてくれたり、無駄なリビルドを抑制したりしてくれます。**

「freezed」は、**State(状態)を「データを保持するだけのクラス」**として**変更不可(イミュータブル)なクラス**として表現することができます。

**Flutter界隈のエンジニアの方々もオススメ**されているので、「流行ってるみたいだし、めっちゃよさげだし、私も乗り遅れないようにしないと」と思いました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">次にやるFlutter案件<br>・state_notifier<br>・freezed<br><br>を使うことにした🙂</p>&mdash; 文字数カウントメモ開発者🎉 takashi (@cloverkizuna) <a href="https://twitter.com/cloverkizuna/status/1261119383079809029?ref_src=twsrc%5Etfw">May 15, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">state_notifierとfreezedパッケージで何かアプリ作ってみようかな！<br><br>StateNotifierを使ったFlutterのアプリ設計 <a href="https://t.co/bDrDGkVopv">https://t.co/bDrDGkVopv</a></p>&mdash; Shogo🏝宮崎 | 対局時計さん⚫️⏰⚪️ (@shogo0525) <a href="https://twitter.com/shogo0525/status/1261935199996137473?ref_src=twsrc%5Etfw">May 17, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Flutter state notifier と freezed の おかけで 死ぬほど楽<br><br>どっかのタイミングでこの知見は共有したい</p>&mdash; 奥村晋太郎 (@120921Shin) <a href="https://twitter.com/120921Shin/status/1260035625224269824?ref_src=twsrc%5Etfw">May 12, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">この前リリースしたアプリがstate_notifierとfreezedで作ってるんだけど、書いてて結構やりやすいと思った。<br><br>けど、あの書き方であってるのかがわからない<br><br>みんなどうやってstate_notifier使ってるんだろ</p>&mdash; としのぶ@個人開発中🚲 (@Toshinobu724) <a href="https://twitter.com/Toshinobu724/status/1262560293390958592?ref_src=twsrc%5Etfw">May 19, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">state_notifierパッケージがとても良いので、それベースに変えた(　´･‿･｀)基本は同じだけど(　´･‿･｀)<br><br>- StateNotifierを基本的に常時利用<br>- StateNotifierが肥大化したら適宜、子StateNotifier的なものに分割して移譲<br>- stateクラスは基本的にfreezed利用<br>- 基本的な扱い方は変わらず <a href="https://t.co/e1C56Es4mp">https://t.co/e1C56Es4mp</a></p>&mdash; mono 🎯 @自宅 💙 (@_mono) <a href="https://twitter.com/_mono/status/1237222683252686853?ref_src=twsrc%5Etfw">March 10, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

私も、実際に使ってみて、**めちゃくちゃ良かった** ですし、**オススメ** です。

そこで本記事では、「state_notifier + freezed」について、カウンターアプリを作りながら説明します。

<img width="656" alt="スクリーンショット 2020-05-18 14.32.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93703/c3623fc8-2697-340b-f210-565e8e4871fa.png">

本記事のコードについて、以下のGitHubリポジトリに公開しています。
https://github.com/karamage/flutter_state_notifier_freezed_sample

## state_notifier について

https://pub.dev/packages/state_notifier
<img width="744" alt="スクリーンショット 2020-05-18 14.42.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93703/d2fac661-7701-db61-90f5-a8294ebee692.png">


state_notifier は、providerでの**状態管理** を、**より簡単に、楽に**するライブラリです。

Flutterで、状態管理のパターンとして、以下のものがよく使われています。

- setState(StatefulWidget)
- Redux
- BLoC(Stream + InheritedWidget/Scoped Model)
- provider(ChangeNotifier)

現時点で、Flutterの状態管理において **ベストプラクティスは、providerを使うこと** だと思います。
(※個人の感想です。Flutterは進化が早いし、流行り廃りが激しい分野なので、これからどうなるかはワカランです。)
**Google公式**においても、**providerの使用を推奨**しています。
[(Pragmatic State Management in Flutter (Google I/O'19))](https://www.youtube.com/watch?v=d_m5csmrf7I) 

**state_notifierは、providerをより使いやすくコードをスッキリと楽に書けるようにしてくれるパッケージ**です。

従来のproviderパターンにおいて、ChangeNotifierを使ってゴリゴリとコードを書いていたのですが、コードが少々**冗長**になり、**Widget階層が深くなりがち**な問題があります。

state_notifierベースでproviderのコードを書くと、
ChangeNotifierを使用した場合よりコードが**スッキリ**し、
コードを書いていて**気持ちが良い**です。

私は、これまでChangeNotifierベースでコードを書いていたのですが、
これからはstate_notifierベースに書き換えていこうと思います。

## freezed について

https://pub.dev/packages/freezed
<img width="772" alt="スクリーンショット 2020-05-18 14.47.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93703/d14c56c8-c7f3-2f17-eb25-8b4e97f47ad1.png">

freezedパッケージは、**イミュータブルなデータモデル**を作成するのに使います。
**Stateは、データを保持するだけのクラスにしたい**ので、 変更不可(イミュータブル/freezed)なクラスにします。
state_notifierでは、State(状態)をイミュータブルなデータモデルとして扱います。
**state_notifierとfreezedは相性が良く、セットで使う**のが推奨されます。
freezedを使うと、Stateに**copyWith(clone)メソッドが自動的に生える**ので、便利です。

## state_notifier と freezed をパッケージインストール

pubspec.yaml を以下のように書きます。

``` pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  provider: ^4.1.0-dev
  state_notifier: ^0.4.0
  flutter_state_notifier: ^0.3.0
  freezed_annotation:

dev_dependencies:
  flutter_test:
    sdk: flutter
  json_serializable:
  build_runner:
  freezed:
```

json_serializableは、今回のアプリではJSONを扱わないので入れなくても良いのですが、JSONの扱う際にfreezedとセットでいれとくと便利です。

ターミナルで以下のコマンドを叩くと、state_notifier と freezed パッケージのインストールが完了します。

```
flutter pub get
```

## カウンターアプリを実装する

ここで、flutter createした際のボイラーテンプレートのカウンターアプリを、「state_notifier + freezed」を使用して実装してみます。

<img width="375" alt="Simulator Screen Shot - iPhone 11 Pro - 2020-05-16 at 17.38.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93703/f5a1d000-6e61-6faa-23fb-c33b1bc70f5d.png">


### CounterStateとCounterStateNotifierを定義する

新規ファイル「counter_state.dart」 を作成し、以下のコードを記述してください。

``` counter_state.dart
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:state_notifier/state_notifier.dart';

part 'counter_state.freezed.dart';
part 'counter_state.g.dart';

@freezed
abstract class CounterState with _$CounterState {
  const factory CounterState({
    @Default(0) int count,
  }) = _CounterState;
  factory CounterState.fromJson(Map<String, dynamic> json) => _$CounterStateFromJson(json);
}

class CounterStateNotifier extends StateNotifier<CounterState> {
  CounterStateNotifier() : super(const CounterState()) {}
  increment() => state = state.copyWith(count: state.count + 1);
}
```

その後、ターミナルで、以下のコマンドを叩いて、「counter_state.freezed.dart」「counter_state.g.dart」を自動生成してください。

```
flutter pub run build_runner build
```


counter_state.dart では、`「CounterState」`クラスと`「CounterStateNotifier」`クラスを定義しています。

#### CounterState

CounterStateは、**状態データの入れ物で、イミュータブルなデータクラス**として定義します。

カウンターの値の状態変数として`「int count」`が定義されています。

デフォルト値を設定するには`「@Default(0) int count」`のように記述します。

`「@freezed」`アノテーションをつけることによって、copyWithメソッドが自動で生えます。copyWithメソッドは状態を更新する場合に、**新しく状態を生成する(clone)ため**に使います。

`「CounterState.fromJson」`メソッドは、今回のカウンターアプリでは使用しませんが、JSONからStateを生成する場合によく使うので記述しています。

#### CounterStateNotifier

CounterStateNotifierは、**状態を操作するロジック**を管理し、Widgetに状態の変更を通知します。

MVVMモデルで解釈すると、**「ViewModel」**に相当します。

`「inclement」`メソッドは、CouterStateのcountを1カウントアップした状態をcopyWithメソッドを使用して、新しく状態を作成し、更新しています。

### CounterStateNotifierを使用したWidgetを作成する

main.dartに以下のコードを記述します。

``` main.dart
import 'package:flutter/material.dart';
import 'package:flutter_state_notifier/flutter_state_notifier.dart';
import 'package:provider/provider.dart';
import 'package:state_notifier_example/counter_state.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: StateNotifierProvider<CounterStateNotifier, CounterState>(
        create: (_) => CounterStateNotifier(),
        child: HomePage(),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('state_notifier sample'),
      ),
      body: Center(
        child: Text(
          context.select<CounterState, int>((state) => state.count).toString(),
        ),
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () => context.read<CounterStateNotifier>().increment(),
        label: Text('1'),
        icon: Icon(Icons.add),
      ),
    );
  }
}

```

Widgetツリーの上層で、StateNotifierProviderを作成すれば、その**子Widgetであればどこでも**、CounterStateNotifierを取得することができます。

ChangeNotifierを使った場合、Consumer等を挟まなくてはいけなかったのですが、state_notifierの場合**スッキリ**書けます。

``` main.dart
      home: StateNotifierProvider<CounterStateNotifier, CounterState>(
        create: (_) => CounterStateNotifier(),
        child: HomePage(),
      ),
```

CounterStateNotifierを取得したい場合、以下のようにすれば、取得できます。

``` main.dart
context.select<CounterState, int>((state) => state.count).toString(),

...

onPressed: () => context.read<CounterStateNotifier>().increment(),

```

context.readとcontext.selectには、以下の違いがあります。

- `context.read<CounterStateNotifier>()`
  - State更新時リビルドしない
  - **StateNotifierのメソッドを使いたいとき**に使う

- `context.select<CounterState, int>()`
  - State更新時リビルドする
  - **状態が変わったときに画面に反映したい場合**に使う

## おわりに

「state_notifier」を使用することにより、**簡単にWidgetから「状態」と「ロジック」を分離**することができました。
「freezed」を使用することにより、Stateをデータを保持するだけのクラス、 変更不可(イミュータブル/freezed)なクラスにすることができました。

**コードがきれいでスッキリして、好きです。**

**超オススメ**ですので、「state_notifier + freezed」を使ってみてはいかがでしょうか。

本記事のコードについて、以下のGitHubリポジトリに公開しています。
https://github.com/karamage/flutter_state_notifier_freezed_sample