Flutter.dev:シンプルなアプリケーション状態管理

こんにちは。OTUSは9月に新しいコース FlutterMobileDeveloper 」を開始します。コース開始の前夜に、私たちは伝統的にあなたのために役立つ翻訳を用意しました。








宣言型ユーザーインターフェイスプログラミングと、エフェメラル状態とアプリケーション状態の違い について理解したので、アプリケーション状態を簡単に管理する方法を学習する準備が整いました。



パッケージを使用しますproviderFlutterを初めて使用し、別のアプローチ(Redux、Rx、フックなど)を選択するやむを得ない理由がない場合は、これがおそらく開始するための最良のアプローチです。パッケージprovider は習得が容易で、多くのコードを必要としません。彼はまた、他のすべてのアプローチに適用できる概念で動作します。



ただし、他のリアクティブフレームワークから状態を管理する経験がすでに豊富な場合は、オプションページにリストされいる他のパッケージやチュートリアルを探すことができます









例として、次の簡単なアプリケーションを考えてみましょう。



このアプリケーションには、カタログとショッピングカート(それぞれウィジェットMyCatalog表されますMyCart)の2つの別々の画面があります。この場合、これはショッピングアプリですが、単純なソーシャルネットワーキングアプリでも同じ構造を想像できます(カタログを「壁」に、カートを「お気に入り」に置き換えます)。



カタログ画面には、カスタマイズ可能なアプリケーションバー(MyAppBar)と複数のリストアイテムのスクロールビュー(MyListItems)が含まれています。



ウィジェットのツリー形式のアプリケーションは次のとおりです。







したがって、少なくとも5つのサブクラスがありWidgetます。それらの多くは、所有していない状態にアクセスする必要があります。たとえば、それぞれMyListItemカートに自分を追加できるはずです。また、現在表示されているアイテムがカートに入っているかどうかを確認する必要がある場合もあります。



これは私たちの最初の質問に私たちをもたらします:私たちはバケットの現在の状態をどこに置くべきですか?



増加する条件



Flutterでは、状態をそれを使用するウィジェットの上に配置するのが理にかなっています。



何のために?Flutterのような宣言型フレームワークでは、ユーザーインターフェイスを変更する場合は、それを再構築する必要があります。ただ行って書くことはできませんMyCart.updateWith(somethingNew)つまり、ウィジェットのメソッドを呼び出して、ウィジェットを外部から強制的に変更することは困難です。そして、それを機能させることができたとしても、それを助けさせるのではなく、フレームワークと戦うことになります。



// :   
void myTapHandler() {
  var cartWidget = somehowGetMyCartWidget();
  cartWidget.updateWith(item);
}




上記のコードが機能MyCartするようになったとしても、ウィジェットで次のことに対処する必要があります



// :   
Widget build(BuildContext context) {
  return SomeWidget(
//   .
  );
}

void updateWith(Item item) {
// -      UI.
}




UIの現在の状態を考慮に入れて、新しいデータを適用する必要があります。ここで間違いを避けるのは難しいでしょう。



Flutterでは、コンテンツが変更されるたびに新しいウィジェットを作成します。MyCart.updateWith(somethingNew)(メソッド呼び出し)の代わりに、MyCart(contents)コンストラクター)を使用します新しいウィジェットは親のビルドメソッドでしか作成できないため、変更contentsする場合は親MyCart以上である必要があります



// 
void myTapHandler(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  cartModel.add(item);
}




これMyCartで、任意のバージョンのユーザーインターフェイスを作成するためのコードパスが1つだけになりました。



// 
Widget build(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  return SomeWidget(
    //     ,    .
    // ···
  );
}




この例では、にあるcontentsはずMyAppです。変更されるたびに、MyCartが一番上に再構築されます(詳細は後で説明します)。このMyCartようにして、ライフサイクルについて心配する必要はありません。特定のコンテンツに対して何を表示するかを宣言するだけです。変更すると、古いウィジェットMyCartは消え、完全に新しいウィジェットに置き換えられます。







これは、ウィジェットが不変であると言うときの意味です。それらは変更されません-それらは置き換えられます。



バケットの状態を配置する場所がわかったので、それにアクセスする方法を見てみましょう。



州のアクセス



ユーザーがカタログ内のアイテムの1つをクリックすると、カートに追加されます。しかし、カートが終わったので、MyListItemこれをどのように行うのですか?



簡単なオプションはMyListItem、クリック時に呼び出すことができるコールバックを提供することです。 Dart関数はファーストクラスのオブジェクトであるため、任意の方法で渡すことができます。したがって、内部的にMyCatalogは、次のように定義できます。



@override
Widget build(BuildContext context) {
  return SomeWidget(
   //  ,      .
    MyListItem(myTapCallback),
  );
}

void myTapCallback(Item item) {
  print('user tapped on $item');
}




これは問題なく機能しますが、さまざまな場所から変更する必要があるアプリケーションの状態では、多くのコールバックを渡す必要があり、すぐに退屈になります。



幸い、Flutterには、ウィジェットがデータとサービスを子孫(つまり、子孫だけでなく、ダウンストリームウィジェット)に提供できるようにするメカニズムがあります。あなたはフラッター、から予想されるようにすべてがウィジェットです:、これらのメカニズムは、単に特別なウィジェットの種類がありInheritedWidgetInheritedNotifierInheritedModelなど。それらは私たちがやろうとしていることとわずかに一致していないため、ここでは説明しません。



代わりに、低レベルのウィジェットで機能するが使いやすいパッケージを使用します。それはと呼ばれproviderます。



を使用すると、providerコールバックやを心配する必要はありませんInheritedWidgetsただし、次の3つの概念を理解する必要があります。



  • ChangeNotifier
  • ChangeNotifierProvider
  • 消費者




ChangeNotifier



ChangeNotifierFlutter SDKに含まれている単純なクラスで、リスナーに状態変更通知を提供します。言い換えれば、何かがあればChangeNotifier、その変更をサブスクライブできます。 (これはObservableの形式です-用語に慣れていない人のために。)



ChangeNotifierInproviderは、アプリケーションの状態をカプセル化する1つの方法です。非常に単純なアプリケーションの場合、1つでうまくいくことができますChangeNotifier。より複雑なモデルでは、いくつかのモデルがあり、したがっていくつかのモデルがありますChangeNotifiers。 (で使用ChangeNotifierする必要はまったくありませんがprovider、このクラスは簡単に操作できます。)



サンプルのショッピングアプリでは、でカートの状態を管理しChangeNotifierます。それを拡張する新しいクラスを作成します。次に例を示します。



class CartModel extends ChangeNotifier {
///    .
  final List<Item> _items = [];

  ///     .
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  ///      ( ,      42 ).
  int get totalPrice => _items.length * 42;

  ///  [item]  .   [removeAll] -     .
  void add(Item item) {
    _items.add(item);
    //    ,    ,   .
    notifyListeners();
  }

  ///     .
  void removeAll() {
    _items.clear();
    //    ,    ,   .
    notifyListeners();
  }
}




に固有の唯一のコードChangeNotifierは呼び出しnotifyListeners()です。アプリケーションのUIに反映できるようにモデルが変更されるたびに、このメソッドを呼び出します。他のすべてCartModelは、モデル自体とそのビジネスロジックです。Flutterの上位レベルのクラスの



ChangeNotifier一部でflutter:foundationあり、依存していません。テストは簡単です(ウィジェットテストを使用する必要はありません)。たとえば、簡単なユニットテストは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



ChangeNotifierProviderChangeNotifierその子にインスタンスを提供するウィジェットです。パッケージに入っていますproviderアクセスが必要なウィジェットの上



に配置する場所はすでにわかっChangeNotifierProviderています。CartModel上記の何かを意味する場合MyCartMyCatalog必要以上



に投稿したくないChangeNotifierProvider(スコープを汚染したくないため)。しかし、私たちの場合、終わった唯一のウィジェットMyCartMyCatalog-それMyApp



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




どうしても必要な場合を除いCartModel. ChangeNotifierProviderて再構築しCartModelないほどスマートな 新しいインスタンスを作成するコンストラクターを定義していることに注意してくださいまた、インスタンスが不要になったときに、CartModelでdispose()を自動的に呼び出します。



複数のクラスを提供する場合は、次を使用できますMultiProvider



void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CartModel()),
        Provider(create: (context) => SomeOtherClass()),
      ],
      child: MyApp(),
    ),
  );
}




消費者



今、それがされていることをCartModel宣言を通じて我々のアプリケーションのウィジェットに提供ChangeNotifierProvider上部に、我々はそれを使用して起動することができます。



これはウィジェットを介して行われますConsumer



return Consumer<CartModel>(
  builder: (context, cart, child) {
    return Text("Total price: ${cart.totalPrice}");
  },
);




アクセスするモデルのタイプを指定する必要があります。この場合、それが必要なのでCartModel、と書きConsumer<CartModel>ます。 generic(<CartModel>)を指定しないと、パッケージproviderは役に立ちません。providerはタイプベースであり、タイプがないと、必要なものを理解できません。



ウィジェットに必要な引数Consumerbuilderだけです。 Builderは、変更時に呼び出される関数ですChangeNotifier。 (つまり、notifyListeners()モデルを呼び出すと、関連するすべてのウィジェットのすべてのビルダーメソッドがConsumer呼び出されます。)



コンストラクターは、3つの引数で呼び出されます。 1つ目はcontextです。これ、すべてのビルドメソッドでも取得できます。

ビルダー関数の2番目の引数はインスタンスですChangeNotifier..。これが私たちが最初から求めていたものです。モデルデータを使用して、ユーザーインターフェイスが特定のポイントをどのように見るかを決定できます。



3番目の引数はchild、最適化に必要です。Consumerモデルが変更されても変更されない大きなウィジェットサブツリーがある場合は、一度ビルドして、ビルダーを介して取得できます。



return Consumer<CartModel>(
  builder: (context, cart, child) => Stack(
        children: [
          //   SomeExhibitedWidget,    .
          child,
          Text("Total price: ${cart.totalPrice}"),
        ],
      ),
  //    .
  child: SomeExpensiveWidget(),
);




コンシューマウィジェットをツリーのできるだけ深い位置に配置することをお勧めします。詳細がどこかで変更されたという理由だけで、ユーザーインターフェイスの大部分を再構築する必要はありません。



//   
return Consumer<CartModel>(
  builder: (context, cart, child) {
    return HumongousWidget(
      // ...
      child: AnotherMonstrousWidget(
        // ...
        child: Text('Total price: ${cart.totalPrice}'),
      ),
    );
  },
);




これの代わりに:



//  
return HumongousWidget(
  // ...
  child: AnotherMonstrousWidget(
    // ...
    child: Consumer<CartModel>(
      builder: (context, cart, child) {
        return Text('Total price: ${cart.totalPrice}');
      },
    ),
  ),
);




Provider.of



ユーザーインターフェイスを変更するためにモデル内のデータが実際には必要ない場合もありますが、それでもアクセスする必要があります。たとえば、ボタンをClearCart使用すると、ユーザーはカートからすべてを削除できます。カートの内容を表示する必要はありませんclear()メソッドを呼び出すだけです。



そのConsumer<CartModel>ために使用することもできますが、それは無駄です。フレームワークにウィジェットを再構築するように依頼しますが、ウィジェットを再構築する必要はありません。



このユースケースではProvider.of、パラメータをにlisten設定して使用できますfalse



Provider.of<CartModel>(context, listen: false).removeAll();




buildメソッドで上記の行を使用しても、が呼び出されnotifyListeners ときにこのウィジェットは再構築されません



すべてを一緒に入れて



この記事で説明されている例確認できます。もう少し簡単なものが必要な場合は、プロバイダーで作成された単純なCounterアプリケーションがどのように見えるかを確認してください自分で



遊ぶ準備ができたら、provider最初にその依存関係を自分のものに追加することを忘れないくださいpubspec.yaml



name: my_name
description: Blah blah blah.

# ...

dependencies:
  flutter:
    sdk: flutter

  provider: ^3.0.0

dev_dependencies:
  # ...




今、あなたはできる'package:provider/provider.dart'; 構築を開始します...






All Articles