LoginSignup
13
8

More than 3 years have passed since last update.

【Flutter】ProxyProvider 入門(サンプルアプリ付き)

Posted at

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

provider | pub.dev

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

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

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

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

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

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

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 のようなところでログインしてしまった場合にその結果を一覧に反映しづらい問題が発生します。

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

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 プロパティに渡した関数です。 AccountModelnotifyListeners() が呼ばれるたびにこの関数が呼ばれ、引数にはすでに作成済みの 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 を使うことで、例えば記事一覧を表す ArticleListPageArticleListModel の状態だけを見ていればよくなります。

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

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

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

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

chooyan-eng/ProxyProviderSample | GitHub

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


  1. BLoC とか Provider とか Notifier などのサフィックスをつける場合もあると思いますが、違いがちゃんと分かっていないためこの記事では provider パッケージの README に倣って Model としています。  

  2. ProxyProvider というクラスも用意されていますが、値の受け渡しが主な用途であれば ChangeNotifierProxyProvider を使うのが便利です。  

13
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
8