【Flutter】Providerを使うための3要素
基本的に公式サイトの翻訳になります。
Provider
を理解するための必要な要素は次の 3 つです。
ChangeNotifier
ChangeNotifierProvider
Consumer
ChangeNotifier
ChangeNotifier
はFlutter SDK
に含まれるシンプルなクラスで、リスナーに変更を通知します。
プロバイダにおいて、ChangeNotifier
はアプリケーションの状態をカプセル化する 1 つの方法になります。非常にシンプルなアプリの場合、1 つのChangeNotifier
で十分です。複雑なアプリではいくつかのモデルを持つことになり、その結果いくつかのChangeNotifier
を持つことになります。
かんたんなショッピングアプリ(商品リストやカートに商品を追加・カートの中身をみる)のカートの状態をChangeNotifier
で管理した場合、以下のようになります。
/// ChangeNotifierを継承しています
class CartModel extends ChangeNotifier {
final List<Item> _items = [];
/// 外部からのカート内のアイテム変更を不可能に
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
/// すべての商品は42$とする
int get totalPrice => _items.length * 42;
void add(Item item) {
_items.add(item);
// このモデルをリッスンしているウィジェットに再びbuildするよう指示します
notifyListeners();
}
void removeAll() {
_items.clear();
// このモデルをリッスンしているウィジェットに再びbuildするよう指示します
notifyListeners();
}
}
ChangeNotifier
に固有のコードはnotifyListeners()
の呼び出しのみです。アプリの UI を変更する可能性のある方法でモデルが変更された時はいつでもこのメソッドを呼び出します。
ChangeNotifier
は flutter:foundation の一部でFlutter
の上位のクラスには依存せず、簡単にテストができます(ウィジェットテストすら必要ありません)。
以下はCartModel
の簡単なユニットテストです。
test('adding item increases total cost', () {
final cart = CartModel();
final startingPrice = cart.totalPrice;
cart.addListener(() {
expect(cart.totalPrice, greaterThan(startingPrice));
});
cart.add(Item('Dash'));
});
ChangeNotifierProvider
ChangeNotifierProvider
は、ChangeNotifier
のインスタンスを知らせるウィジェットです。
プロバイダパッケージから提供されます。
ChangeNotifierProvider
をどこに置くかはすでに決まっています。アクセスする必要があるウィジェットの上です。CartModel
の場合、それはMyCart
とMyCatalog
両方の上位です。
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MyApp(),
),
);
}
CartModel
の新しいインスタンスを作成するビルダーを定義していることに注意してください。ChangeNotifierProvider
は賢いので、必要でない限りCartModel
を再構築することはありません。
また、インスタンスが不要になると自動的にCartModel
のdispose()
を呼び出します。
もし、複数のクラスを提供したい場合は、MultiProvider
を使用できます。
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CartModel()),
Provider(create: (context) => SomeOtherClass()),
],
child: const MyApp(),
),
);
}
Consumer
冒頭のChangeNotifierProvider
により、アプリ内のウィジェットにCartModel
が伝えられました。
早速使ってみましょう。
Consumer
ウィジェットを通じて行われます。
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
);
アクセスしたいモデルのタイプを指定する必要があります。今回はCartModel
が欲しいので、Consumer<CartModel>
と書いています。
プロバイダは型をベースにしており、型がなければ何をしたいのかはわかりません。
Consumer
ウィジェット唯一の必須引数はbuilder
です。Builder
は、ChangeNotifier
が変更されるたびに呼び出される関数です。言い換えれば、モデル内でnotifyListeners()
を呼び出すと対応するすべての Consumer
ウィジェットのbuilder
メソッドが呼び出されます。
ビルダーは 3 つの引数で呼び出されます。
最初の引数はcontext
で、これはすべてのビルドメソッドで渡されます。
2 番目引数は、ChangeNotifier
のインスタンスです。
モデル内のデータを使って、任意の時点でUI
がどのように見えるべきかを定義できます。
3 番目の引数はchild
で、これは最適化のために存在します。
Consumer
の下に、モデルが変わっても変化しない大きなウィジェットのサブツリーがある場合、それを一度構築してビルダーを通して取得できます。
return Consumer<CartModel>(
builder: (context, cart, child) => Stack(
children: [
// ここでSomeExpensiveWidgetを使用すると、毎回再構築することなく使用できます。
if (child != null) child,
Text('Total price: ${cart.totalPrice}'),
],
),
// expensiveウィジェットをここで構築する
child: const SomeExpensiveWidget(),
);
Consumer
ウィジェットはできるだけツリーの奥に配置するのがベストプラクティスです。
どこかが変わったからといってUI
の大部分を作り直したくはないでしょう。
// NG
return Consumer<CartModel>(
builder: (context, cart, child) {
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Text('Total price: ${cart.totalPrice}'),
),
);
},
);
// GOOD
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
),
),
);
Provider.of
UI
を変更するためにモデル内のデータを必要としないが、アクセスする必要がある場合はあります。
たとえば、ClearCart
ボタンはユーザがカートからすべてを削除できるようにしたいとします。
カートの中身を表示する必要はなく、clear()
メソッドを呼び出すだけでよいのです。
このためにConsumer<CartModel>
を使用することもますが、それはむだなことです。
再構築する必要のないウィジェットを再構築するようにフレームワークに要求することになります。
この使用例では、listenパラメータ
をfalse
に設定したProvider.of
を使用できます。
Provider.of<CartModel>(context, listen: false).removeAll();
上記の行をビルドメソッドで使用すると、notifyListeners が呼ばれたときにこのウィジェットを再構築することはありません。
まとめ
本記事ではProvider
を用いたかんたんな説明しか書いていませんが、もちろんドキュメントの前後のページには他の状態管理の方法についても詳しく書かれています。
この記事で取り上げたサンプルコードを置いておきます。
というかサンプルコード見ないとイメージつかないかもです。
(公式のソースを和訳してちょっといじっただけです)
Discussion