【Flutter】ProxyProvider 入門（サンプルアプリ付き）

Flutter には `provider` パッケージという便利なものがあります。

[provider | pub.dev](https://pub.dev/packages/provider)

`InheritedWidget` のラッパーとして作られていて、Widget から状態やロジックを切り離したり、DI的なことが簡単に実現できるアレですね。

# Model を別の Model に依存させたい

`provider` パッケージを使っていると、ある Model （`Provider` を使って Widget に受け渡す、`ChangeNotifier` などを継承したオブジェクト[^1]）が管理するデータを他の Model の管理するデータに依存させたい場合があります。

例えば、アカウントの状態を管理する `AccountModel` と記事一覧を管理する `ArticleListModel` があったとして、「ログイン前とログイン後で表示する記事一覧を変えたい」という要件を実装したい場合、通常の `Provider` では意外と実装が困難です。

何も考えずに `AccountModel` を実装しようとすると以下のコード中のコメントような問題が発生します。

※ 見やすさのため、記事中のコードはいろいろと割愛しています

```dart:account_model.dart
class AccountModel {
  Member _member;

  Member get member => _member;
  set member(Member value) {
    _member = value;
    notifyListeners();
  }

  bool get isMember => member != null;

  /// ログイン処理。ログインしたら記事一覧に会員専用の記事も表示したい。
  Future<LoginResponse> login(String id, String password) async {
    final response = await LoginRequest().login(id, password);
    if (response.result == LoginResult.successful) {
      member = response.member;
      // ログインできたからなんとかして ArticleListModel の中身を書き換えたいけど
      // context も無いしあったとしても ArticleListModel にアクセスできるやつか分からないので
      // ArticleListModel が管理する記事一覧を変更できない。
    }
    return response;
  }
}
```

結果的に `login()` を呼び出した Widget 側で結果を見て `ArticleListModel` を操作する感じになるのですが、それはそれで記事一覧画面とは全く関係のない `LoginPage` のようなところでログインしてしまった場合にその結果を一覧に反映しづらい問題が発生します。

```dart:login_page.dart
class LoginPage extends StatelessWidget {
  Future<void> _login() async {
    final response = await Provider.of<AccountModel>(context, listen: false).login(_id, _password);
    if (response.result == LoginResult.successful) {
      // ArticleListModel とは全然関係ないし子孫でもないので、やっぱり記事一覧を変更できない
    }
  }
}
```

ここで登場するのが `ProxyProvider` です。

# `ProxyProvider` とは

参考： [ProxyProvider | provider](https://pub.dev/packages/provider#proxyprovider)

`ProxyProvider` は、 `provider` パッケージのバージョン 3.0.0 から追加された `Provider` で、 __ある Model で発生した変更を他の Model に通知する__ 機能を持っています。

> Since the 3.0.0, there is a new kind of provider: ProxyProvider.
ProxyProvider is a provider that combines multiple values from other providers into a new object, and sends the result to Provider.

つまり、先ほどの「ログインしたら記事一覧を更新する」というような仕組みがきれいに作れる、ということですね。

細かな使い方はドキュメントをご覧いただければと思いますが、以下のような感じで実装できます。

```main.dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ChangeNotifierProvider(
        create: (context) => AccountModel(), // AccountModel を通常通り各 Widget で使えるようにする
        child: Scaffold(
          body: ChangeNotifierProxyProvider<AccountModel, ArticleListModel>( // 子孫で ProxyProvider を使って AccountModel の変更を受け取れるようにする。
            create: (context) => ArticleListModel(),
            update: (context, accountModel, articleListModel) { // AccountModel で notifiListeners() が呼ばれるたびにこの update() が呼び出される
              articleListModel.updateList(accountModel.isMember); // ログイン状態に応じて記事一覧を修正 
              return articleListModel;
            },
            child: ArticleListPage(),
          ),
        ),
      ),
    );
  }
}
```

まずは `AccountModel` を従来通り `ChangeNotifierProvider` で各 Widget からアクセス可能な状態にします。

次に `ChangeNotifierProxyProvider` を使うことで[^2] `ArticleListModel` 自身が `AccountModel` の変更を受け取れるようになります。

変更を受け取るのは　`update` プロパティに渡した関数です。 `AccountModel` で `notifyListeners()` が呼ばれるたびにこの関数が呼ばれ、引数にはすでに作成済みの `AccountModel` インスタンスと `ArticleListModel` インスタンスが渡されるため、ここでそれぞれのインスタンスを使って自由に処理できる、というわけです。

上記のコードでは `AccountModel` からログイン済みかどうかのフラグを取ってきて `ArticleListModel` の記事一覧をアップデートしていますね。

 `update` に指定する関数は、戻り値として他の Model に依存する Model （今回は ArticleListModel）のインスタンスを返却する必要がありますので、通常は引数で受け取ったインスタンスをそのまま返却する感じになるかと思います。

# `ProxyProvider` を使うメリット

`ProxyProvider` を使うことで、以下のようなメリットが生まれます。

## Model の中に `context` や他の Model インスタンスを保持する必要がない

実装を見ると分かりますが、 `AccountModel` はログインが完了したら自身が保持するログイン状態を更新して `notifyListeners()` を呼ぶだけです。

また、 `ArticleListModel` の方はログイン状態に応じて記事一覧を更新する関数を用意しておくだけで、 `AccountModel` については何も知りません。

またそれぞれの Model にアクセスするための `context` も保持する必要がありません。

これにより、 Model は他の Model のことを気にせず自身が保持すべき状態の管理だけに集中でき、実装がとても疎結合になります。

## Widget が頑張らない

`ProxyProvier` を使わない場合、 View 側で工夫して片方の Model の変更を検知して別の Model を変更するようなコードを書かなければなりません。

しかし `ProxyProvider` を使うことで、例えば記事一覧を表す `ArticleListPage` は `ArticleListModel` の状態だけを見ていればよくなります。

 `ArticleListPage` はログイン状態が何かのタイミングで変わったかどうかを気にすることなく、ただ `ArticleListModel` が発行する一覧データ変更の通知だけを検知できれば良いので、ログインに関する特別なロジックを一切書かなくて済む、ということです。

# まとめとサンプルプロジェクト

以上、 `ProxyProvider` の基本とそのメリットについて説明しました。

ここまでの説明で使った「ログイン状態によって表示する記事一覧が変化するアプリ」のサンプルは以下に公開しています。

[chooyan-eng/ProxyProviderSample | GitHub](https://github.com/chooyan-eng/ProxyProviderSample)

アプリが大きくなってくると、ログイン状態や通知の状態など、個別の画面に関係なく変化してUIに影響を与える状態というものが増えてきます。その変化を簡潔に Model から Model へ伝える手段として `ProxyProvider` というものが用意されていますので、積極的に使っていくと良いでしょう。

[^1]: `BLoC` とか `Provider` とか `Notifier` などのサフィックスをつける場合もあると思いますが、違いがちゃんと分かっていないためこの記事では `provider` パッケージの README に倣って `Model` としています。 
[^2]: `ProxyProvider` というクラスも用意されていますが、値の受け渡しが主な用途であれば `ChangeNotifierProxyProvider` を使うのが便利です。