Flutter state_notifierいい感じなので使ったほうが良いですよ

[provider](https://pub.dev/packages/provider)パッケージの作者である[Remi Rousselet](https://twitter.com/remi_rousselet)さんが今月リリースした
[flutter_state_notifier](https://pub.dev/packages/flutter_state_notifier)パッケージが日本のFlutter界隈でも話題になっていますね

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">state_notifier、日本人の貢献が活発🇯🇵🙌 with <a href="https://twitter.com/heavenOSK?ref_src=twsrc%5Etfw">@heavenOSK</a> <a href="https://t.co/dG8HCs0QFH">https://t.co/dG8HCs0QFH</a> <a href="https://t.co/gtian8p8yd">pic.twitter.com/gtian8p8yd</a></p>&mdash; mono 🎯 @自宅 💙 (@_mono) <a href="https://twitter.com/_mono/status/1236603427351744512?ref_src=twsrc%5Etfw">March 8, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

日本で有名なFlutterユーザの方達もコミットされていますね

[まだ BLoC で消耗してるの？](https://ja.unflf.com/tech/flutter/provider/)というブログもありましたが、
`change_notifier`ベースの書き方より気持ちいいので、今後は`state_notifier`ベースが増えると思います

## 使い方

### pubspec.yaml
stateを同作者による[freezed](https://pub.dev/packages/freezed)を使って書くと気持ちがいいです
僕が書いたfreezedの雑な解説記事は[こちら](https://qiita.com/_masaokb/items/19a93be6079a80ac2d85)にあります

ちなみに、[state_notifier](https://pub.dev/packages/state_notifier)はFlutterに依存しないパッケージで、[flutter_state_notifier](https://pub.dev/packages/flutter_state_notifier)がFlutterと統合されたものになっています（具体的にはProviderやLocatorMixinを挟み込む部分をやってくれている）
ので、Flutterで用いる場合は後者で

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

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

### 基本的な考え方

- ViewにはViewのことしか書きたくない
- ネストしたくない（StreamBuidlerやProvider.Consumer/Selectorを使いたくない）
- 安全に状態を管理したい（Viewからは直接いじれないようにしたい）


### 書き方の例
`HogeStateNotifier`の部分の命名は`ViewModel`だったり`Controller`だったり色々考えられると思うのですが、
いったん[本家のexample](https://github.com/rrousselGit/state_notifier/blob/master/example/lib/my_state_notifier.dart)に合わせました
好みで変えていいと思います

> #### 命名について追記
> 
> <blockquote class="twitter-tweet"><p lang="en" dir="ltr">Counter vs CounterNotifier works too</p>&mdash; Remi Rousselet (@remi_rousselet) <a href="https://twitter.com/remi_rousselet/status/1239489899360915456?ref_src=twsrc%5Etfw">March 16, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


- hoge_page.dart
    - `HogePage`
        - 状態を反映したビュー
- hoge_state.dart
    - `HogeState`
        - 状態を持つイミュータブルなクラス
    - `HogeStateNotifier`
        - Stateを操作するクラス
- hoge_state.freezed.dart
    - 自動生成される

以下、それぞれのクラスについて見ていきます
（仕事中にいそいそ書いてるのでコードは全て疑似コードで、ツッコミは受け付けません）

#### State

```dart:hoge_state.dart
@freezed
abstract class HogeState with _$HogeState {
  const factory HogeState({
    @Default(0) int page,
    @Default(false) bool isEnabled,
    String content
  }) = _HogeDetailState;
}
```

freezedベースのイミュータブルなクラスです。
そのビューについての、**変更を画面に通知したい**変数を集約させているイメージです
`@required`を使うよりは`@Default`で初期値を与えるのがクールだと思います（`page = 0`でないのに注意）
後述しますが更新時は`state = state.copyWith(page: 1)`のような感じで

#### StateNotifier

```dart:hoge_state.dart
class HogeStateNotifier extends StateNotifier<HogeDetailState> {
  final int hogeId;
  String draft = '';
  bool _isLoading = false;

  HogeStateNotifier({@required this.hogeId}): super(const HogeState()) {
    fetch();
  }

  void setPage(int value) {
    state = state.copyWith(page: value);
    state = state.copyWith(isEnabled: _isEnabled);
  }

  Future<void> fetch() {
    if (_isLoading) return;
    // いい感じにfetchしてるとする
    state = state.copyWith(content: content);
  }

  bool _isEnabled() {
    // stateをもとにいい感じに計算してるとする
    return true;
  }
}
```
stateを内部に持つ、ロジック部分を担当するクラスです
ここで注目してほしいのは、

- **変更を画面に通知したいわけではない**変数はStateNotifierが持っている
  - ここでは`draft`、例えば遷移時に次の画面にわたす変数だとしましょう
- ロジックの内部でのみ使う変数もprivateでStateNotifierが持っている
    - ここでは`_isLoading`
- stateの更新は全てStateNotifierで行う。Viewから弄りたいときはアクセサを生やす
    - ここでは`setPage`

点ですね

あと、好みだと思いますが、僕は複雑な遷移処理（例えばbottomSheetを出し、そこでの反応に応じて画面遷移する、など）もStateNotifierに書いてしまっています
Viewを複雑にしたくないという気持ちがあります

```dart:hoge_state.dart
  Future<void> pushToWebView(BuildContext context) async {
    // 遷移処理やモーダル出したりの処理
  }
```

あとこれも好みでしょうが、孫Viewからも参照したりしそうな`HogeController`の類もここで持っています（disposeを忘れないように）

#### Page

**親のbuild**

```dart:hoge_page.dart
  @override
  Widget build(BuildContext context) {
    return StateNotifierProvider<HogeStateNotifier, HogeState>(
      child: const _Scaffold(),
      create: (BuildContext context) => HogeStateNotifier(hogeId: widget.hogeId),
    );
  }
```

**子での参照**

以下を使い分けネストとリビルドの少ないコードを書きましょう

- `context.read<HogeStateNotifier>()`
    - State更新時に**リビルドされない**
    - StateNotifierのメソッドを呼びたい、Stateの値を更新したい時など
- `context.read<HogeState>()`
    - State更新時に**リビルドされない**
    - Stateの現在の値を参照したい（画面遷移で渡すなど）
- `context.select<HogeState, FugaClass>((s) => s.fuga)`
    - State更新時、**fugaが変化している場合のみリビルドされる**
    - `context.select((HogeState s) => s.fuga)`でもいいよ
    - 状態の変化をViewに反映させたいとき
- `context.watch<HogeState>()`
    - State更新時、**常にリビルドされる**
    - あまり使わないイメージ

### スニペット
AndroidStudioでは以下をLive Templateに登録しておくと開発が捗りそう
命名に応じて変えて下さい

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

part '$FILE_NAME$_state.freezed.dart';

@freezed
abstract class $CLASS_NAME$State with _$$$CLASS_NAME$State {
  const factory $CLASS_NAME$State({}) = _$CLASS_NAME$State;
}

class $CLASS_NAME$StateNotifier extends StateNotifier<$CLASS_NAME$State> {
  $CLASS_NAME$StateNotifier() : super(const $CLASS_NAME$State()) {}
}
```

## 結論
StateとNotifierを分離したことが`ChangeNotifier`と比べた際の大きなメリットだと思います
`notifyListener()`を忘れることもなくなるし…

僕はこれから徐々に業務コードのリプレイスをやっていきます
皆さんはどうですか？