Flutter には provider
パッケージという便利なものがあります。
InheritedWidget
のラッパーとして作られていて、Widget から状態やロジックを切り離したり、DI的なことが簡単に実現できるアレですね。
Model を別の Model に依存させたい
provider
パッケージを使っていると、ある Model (Provider
を使って Widget に受け渡す、ChangeNotifier
などを継承したオブジェクト1)が管理するデータを他の Model の管理するデータに依存させたい場合があります。
例えば、アカウントの状態を管理する AccountModel
と記事一覧を管理する ArticleListModel
があったとして、「ログイン前とログイン後で表示する記事一覧を変えたい」という要件を実装したい場合、通常の Provider
では意外と実装が困難です。
何も考えずに AccountModel
を実装しようとすると以下のコード中のコメントような問題が発生します。
※ 見やすさのため、記事中のコードはいろいろと割愛しています
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
のようなところでログインしてしまった場合にその結果を一覧に反映しづらい問題が発生します。
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
パッケージのバージョン 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.
つまり、先ほどの「ログインしたら記事一覧を更新する」というような仕組みがきれいに作れる、ということですね。
細かな使い方はドキュメントをご覧いただければと思いますが、以下のような感じで実装できます。
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
アプリが大きくなってくると、ログイン状態や通知の状態など、個別の画面に関係なく変化してUIに影響を与える状態というものが増えてきます。その変化を簡潔に Model から Model へ伝える手段として ProxyProvider
というものが用意されていますので、積極的に使っていくと良いでしょう。