Flutterでズンドコ (ChangeNotifier)




懐かしいネタをDartで書かれている方がいた([こちらの記事](https://qiita.com/satoshi-baba-0823/items/e48aeb376cd7e8a91ba1))ので、自分もやってみました。

# 元ネタ


<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr">Javaの講義、試験が「自作関数を作り記述しなさい」って問題だったから<br>「ズン」「ドコ」のいずれかをランダムで出力し続けて「ズン」「ズン」「ズン」「ズン」「ドコ」の配列が出たら「キ・ヨ・シ！」って出力した後終了って関数作ったら満点で単位貰ってた</p>&mdash; てくも (@kumiromilk) <a href="https://twitter.com/kumiromilk/status/707437861881180160">March 9, 2016</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>


# 完成品

![zundoko.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266232/7765ced1-7350-119e-c56c-25a18b269ce5.gif)


# コード全体
最初にコード全体を載せておきます。(`import`文などは、省略しています。)
どうしてもネストが深くなりがちなFlutterのコードですが、
以下のように、__Widgetを細かくクラスに切り出す__ことで、見通しの良いコードになると思います。

```dart

// ViewModel
class Kiyoshi with ChangeNotifier {
  String text = "";

  void sing() {
    if (text.endsWith("ズンズンズンズンドコ")) {
      text = "キ・ヨ・シ！";
    } else {
      if (text == "キ・ヨ・シ！") text = "";
      text += Random().nextBool() ? "ズン" : "ドコ";
    }
    notifyListeners();
  }
}

// ページ
class KiyoshiPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<Kiyoshi>(
      create: (_) => Kiyoshi(),
      child: Scaffold(
        body: Center(child: KiyoshiText()),
        floatingActionButton: KiyoshiButton(),
      ),
    );
  }
}

// ボタン
class KiyoshiButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final kiyoshi = Provider.of<Kiyoshi>(context, listen: false);
    return FloatingActionButton(
      child: Icon(Icons.add),
      onPressed: () => kiyoshi.sing(),
    );
  }
}

// テキスト
class KiyoshiText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final kiyoshi = Provider.of<Kiyoshi>(context);
    return Text(kiyoshi.text);
  }
}
```

# 簡単な説明

## ViewModelの構成
`ChangeNotifier`を`mixin`するだけで、ViewModelとして機能します。
ロジック実行後に`notifyListeners()`を呼ぶことで、__View側に変更を通知__できます。
ロジック自体は適当なので、説明は省略します。

```dart
class Kiyoshi with ChangeNotifier {
  String text = "";

  void sing() {
    ...省略

    // View側に変更を通知
    notifyListeners();
  }
}
```

## Widgetの構成

### 1. ViewModelをツリー上で使えるようにする  
`ChangeNotifierProvider`を使って、先ほど作った`Kiyoshi`を注入します。
これで、`KiyoshiPage`の下位ツリー全体で__同じ`Kiyoshi`のインスタンスにアクセスできる__ようになります。

```dart
class KiyoshiPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<Kiyoshi>(
      create: (_) => Kiyoshi(),
      child: ...
    );
  }
}
```

### 2. テキスト部分を作る
`Provider.of()`を使うことで、先ほど注入した`Kiyoshi`のインスタンスを*O(1)*で取得できます。
この時、このWidgetはリスナーとして登録され、`notifyListeners()`が呼ばれる度に自動でリビルドされるようになります。


```dart
class KiyoshiText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final kiyoshi = Provider.of<Kiyoshi>(context);
    return Text(kiyoshi.text);
  }
}
```

また、以下のように、`Consumer`を使って書くこともできます。(こちらのほうが一般的かも)

```dart
class KiyoshiText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<Kiyoshi>(
      builder: (context, kiyoshi, child) {
        return Text(kiyoshi.text);
      },
    );
  }
}
```

### 3. ボタン部分を作る
やっていることは、テキスト部分と同じです。
ただし、ボタンはテキストと違い毎回リビルドする必要はないので、`listen: false`を設定しています。

```dart
class KiyoshiButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //                                               ↓ココ重要
    final kiyoshi = Provider.of<Kiyoshi>(context, listen: false);
    return FloatingActionButton(
      child: Icon(Icons.add),
      onPressed: () => kiyoshi.sing(),
    );
  }
}
```

# ChangeNotifierを使った感想

### 1. かんたん
ReduxやBLoCパターンと比べると、圧倒的にわかりやすいです。
また、ボイラープレートがほとんどなく、サクサク書けます。

### 2. 使ってる人が多い
Flutterのステートマネジメントの中では、かなり人気の構成です。
検索すれば、たくさん記事やサンプルコードが出てきます。(まだまだ日本語記事は少ないですが)

### 3. 他のアーキテクチャと併用も良さそう
自分は普段、BLoCパターンを使っているのですが、
基本はBLoCパターンで実装して、Themeの切り替えなど、単純なロジックのみ`Provider` + `ChangeNotifier`で実装といった構成も有りかなと思いました。

# ChangeNotifierを使う際の注意点
[公式リファレンス](https://api.flutter.dev/flutter/foundation/ChangeNotifier-class.html)を読むと、

>ChangeNotifier is optimized for small numbers (one or two) of listeners. It is O(N) for adding and removing listeners and O(N²) for dispatching notifications (where N is the number of listeners).

と書かれています。
翻訳にかけると、
>
ChangeNotifierは、少数（1つまたは2つ）のリスナー用に最適化されています。リスナーを追加および削除する場合はO（N）、通知をディスパッチする場合はO（N²）です（Nはリスナーの数です）。

だそうです。
つまり、__「listenするWidgetの数が多い場合には適していない」__ようなので注意が必要です。