🐬

【Flutter】Providerを使うための3要素

2022/07/31に公開約5,100字

はじめに

22新卒でエンジニア就職した者ですが、7月でやっとすべての研修が終了しました。
配属先はこれまで経験のないモバイルアプリチーム。会社の変革期ということもあり、運良く『モバイルアプリのFlutter一本化』のプロジェクトに参画させていただくことになりました。

バックエンド主体で開発をしてきたのでFlutterはいろいろと慣れない部分が多く、情報の整理が必要と感じています。というわけで学習メモを残すことにしました。

基本的に公式サイトの翻訳になります。

https://docs.flutter.dev/development/data-and-backend/state-mgmt/intro

Providerを理解するための必要な要素は次の3つです。

  • ChangeNotifier
  • ChangeNotifierProvider
  • Consumer

ChangeNotifier

ChangeNotifierFlutter 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の場合、それはMyCartMyCatalog両方の上位です。

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CartModel(),
      child: const MyApp(),
    ),
  );
}

CartModelの新しいインスタンスを作成するビルダーを定義していることに注意してください。ChangeNotifierProviderは賢いので、必要でない限りCartModelを再構築することはありません。
また、インスタンスが不要になると自動的にCartModeldispose()を呼び出します。

もし、複数のクラスを提供したい場合は、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を用いたかんたんな説明しか書いていませんが、もちろんドキュメントの前後のページには他の状態管理の方法についても詳しく書かれています。

この記事で取り上げたサンプルコードを置いておきます。
というかサンプルコード見ないとイメージつかないかもです。

https://github.com/sakanafuto/provider_shopper

公式のソースを和訳してちょっといじっただけです)

GitHubで編集を提案

Discussion

ログインするとコメントできます