Flutterプロバイダーのネストされたオブジェクト

4
JBM 2019-12-17 13:19.

Flutterアプリの状態を管理するためにプロバイダーパッケージを使用しています。オブジェクトのネストを開始すると、問題が発生します。

非常に簡単な例:親AにはタイプBの子があり、タイプCの子にはタイプDの子があります。子Dでは、色属性を管理したいと思います。以下のコード例:

import 'package:flutter/material.dart';

class A with ChangeNotifier
{
    A() {_b = B();}

    B _b;
    B get b => _b;

    set b(B value)
    {
        _b = value;
        notifyListeners();
    }
}

class B with ChangeNotifier
{
    B() {_c = C();}

    C _c;
    C get c => _c;

    set c(C value)
    {
        _c = value;
        notifyListeners();
    }
}

class C with ChangeNotifier
{
    C() {_d = D();}

    D _d;
    D get d => _d;

    set d(D value)
    {
        _d = value;
        notifyListeners();
    }
}

class D with ChangeNotifier
{
    int                 _ColorIndex = 0;
    final List<Color>   _ColorList = [
        Colors.black,
        Colors.blue,
        Colors.green,
        Colors.purpleAccent
    ];

    D()
    {
        _color = Colors.red;
    }

    void ChangeColor()
    {
        if(_ColorIndex < _ColorList.length - 1)
        {
            _ColorIndex++;
        }
        else
        {
            _ColorIndex = 0;
        }

        color = _ColorList[_ColorIndex];
    }

    Color _color;

    Color get color => _color;

    set color(Color value)
    {
        _color = value;
        notifyListeners();
    }
}

これで、main.dartPlaceholder()ウィジェットを管理している)に次のものが含まれます。

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_example/NestedObjects.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget
{
    @override
    Widget build(BuildContext context)
    {
        return MaterialApp(
            home: ChangeNotifierProvider<A>(
                builder: (context) => A(),
                child: MyHomePage()
            ),
        );
    }
}

class MyHomePage extends StatefulWidget
{

    @override
    State createState()
    {
        return _MyHomePageState();
    }
}

class _MyHomePageState extends State<MyHomePage>
{
    @override
    Widget build(BuildContext context)
    {
        A   a = Provider.of<A>(context);
        B   b = a.b;
        C   c = b.c;
        D   d = c.d;

        return Scaffold(
            body: Center(
                child: Column(
                    children: <Widget>[
                        Text(
                            'Current selected Color',
                        ),
                        Placeholder(color: d.color,),
                    ],
                ),
            ),
            floatingActionButton: FloatingActionButton(
                onPressed: () => ButtonPressed(context),
                tooltip: 'Increment',
                child: Icon(Icons.arrow_forward),
            ),
        );
    }

    void ButtonPressed(BuildContext aContext)
    {
        A   a = Provider.of<A>(context);
        B   b = a.b;
        C   c = b.c;
        D   d = c.d;

        d.ChangeColor();
    }
}

上記は、プレースホルダーウィジェットの色属性がクラスDの色プロパティによって定義されていることを示しています(A -> B -> C -> D.color)。上記のコードは非常に単純化されていますが、私が抱えている問題を示しています。

バックポイントに:どのように私は割り当てます子供D「ウィジェットへの色のプロパティ、更新時にそのことを子供Dのプロパティを、それも自動的に(使用してウィジェットを更新しnotifyListeners()、ではありませんsetState())。

StatelessStatefulProvider.ofConsumerを使用しましたが、すべて同じ結果が得られます。繰り返しになりますが、オブジェクトを分離することはできません。親子関係が必要です。


編集

より複雑な例:

import 'dart:ui';

enum Manufacturer
{
    Airbus, Boeing, Embraer;
}

class Fleet
{
    List<Aircraft> Aircrafts;
}

class Aircraft
{
    Manufacturer        AircraftManufacturer;
    double              EmptyWeight;
    double              Length;
    List<Seat>          Seats;
    Map<int,CrewMember> CrewMembers;
}

class CrewMember
{
    String Name;
    String Surname;
}

class Seat
{
    int     Row;
    Color   SeatColor;
}

上記のコードは、実際の例を簡略化したものです。ご想像のとおり、うさぎの穴はどんどん深くなっていきます。つまり、AスルーのD例で意味したのは、状況の畳み込みを単純化しようとしたことです。

たとえば、ウィジェットで乗組員の名前を表示および/または変更したいとします。アプリ自体では、一般的に選択することになるAircraftからFleet(でウィジェットに渡さList選択し、インデックス)CrewMemberからAircraft(によって渡されたMapキー)をしてから表示/変更するNameのをCrewMember

最終的に、ウィジェットは、渡されたAircraftsインデックスとCrewMembersキーを使用して、参照しているクルーメンバーの名前を確認できるようになります。

私は間違いなく、より良いアーキテクチャとデザインを受け入れています。

2 answers

4
Frank Treacy 2019-12-18 04:22.

編集:更新された質問への回答、以下の元の

それはどのような明確ではありませんでしたABCそしてDあなたの元の質問に立っていました。それらはモデルであることが判明しました。

私の現在の考えは、モデルではなく、サービスを提供するためにアプリをMultiProvider/でラップすることです。ProxyProvider

データをどのようにロードしているかはわかりませんが(あるとしても)、フリートを非同期にフェッチするサービスを想定しました。データがパーツ/モデルによって(一度にではなく)異なるサービスを介してロードされる場合、それらをに追加し、MultiProviderさらにデータをロードする必要があるときに適切なウィジェットに挿入することができます。

以下の例は完全に機能しています。簡単にするために、そしてname例として更新について質問されたので、私はそのプロパティセッターのみを作成しましたnotifyListeners()

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

main() {
  runApp(
    MultiProvider(
      providers: [Provider.value(value: Service())],
      child: MyApp()
    )
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Consumer<Service>(
            builder: (context, service, _) {
              return FutureBuilder<Fleet>(
                future: service.getFleet(), // might want to memoize this future
                builder: (context, snapshot) {
                  if (snapshot.hasData) {
                    final member = snapshot.data.aircrafts[0].crewMembers[1];
                    return ShowCrewWidget(member);
                  } else {
                    return CircularProgressIndicator();
                  }
                }
              );
            }
          ),
        ),
      ),
    );
  }
}

class ShowCrewWidget extends StatelessWidget {

  ShowCrewWidget(this._member);

  final CrewMember _member;

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<CrewMember>(
      create: (_) => _member,
      child: Consumer<CrewMember>(
        builder: (_, model, __) {
          return GestureDetector(
            onDoubleTap: () => model.name = 'Peter',
            child: Text(model.name)
          );
        },
      ),
    );
  }
}

enum Manufacturer {
    Airbus, Boeing, Embraer
}

class Fleet extends ChangeNotifier {
    List<Aircraft> aircrafts = [];
}

class Aircraft extends ChangeNotifier {
    Manufacturer        aircraftManufacturer;
    double              emptyWeight;
    double              length;
    List<Seat>          seats;
    Map<int,CrewMember> crewMembers;
}

class CrewMember extends ChangeNotifier {
  CrewMember(this._name);

  String _name;
  String surname;

  String get name => _name;
  set name(String value) {
    _name = value;
    notifyListeners();
  }

}

class Seat extends ChangeNotifier {
  int row;
  Color seatColor;
}

class Service {

  Future<Fleet> getFleet() {
    final c1 = CrewMember('Mary');
    final c2 = CrewMember('John');
    final a1 = Aircraft()..crewMembers = { 0: c1, 1: c2 };
    final f1 = Fleet()..aircrafts.add(a1);
    return Future.delayed(Duration(seconds: 2), () => f1);
  }

}

アプリを実行し、データが読み込まれるまで2秒間待つと、そのマップにid = 1の乗組員である「John」が表示されます。次に、テキストをダブルタップすると、「Peter」に更新されます。

お気づきのように、私はサービスのトップレベルの登録(Provider.value(value: Service()))とモデルのローカルレベルの登録(ChangeNotifierProvider<CrewMember>(create: ...))を使用しています。

このアーキテクチャ(適度な量のモデルを使用)は実現可能であると思います。

ローカルレベルのプロバイダーに関しては、少し冗長だと思いますが、短くする方法があるかもしれません。また、変更を通知するためのセッターを備えたモデル用のコード生成ライブラリがあると便利です。

(C#のバックグラウンドはありますか?クラスをDart構文に一致するように修正しました。)

これがあなたのために働くかどうか私に知らせてください。


プロバイダーを使用する場合は、プロバイダーを使用して依存関係グラフを作成する必要があります。

(セッターインジェクションの代わりにコンストラクターインジェクションを選択できます)

これは機能します:

main() {
  runApp(MultiProvider(
    providers: [
        ChangeNotifierProvider<D>(create: (_) => D()),
        ChangeNotifierProxyProvider<D, C>(
          create: (_) => C(),
          update: (_, d, c) => c..d=d
        ),
        ChangeNotifierProxyProvider<C, B>(
          create: (_) => B(),
          update: (_, c, b) => b..c=c
        ),
        ChangeNotifierProxyProvider<B, A>(
          create: (_) => A(),
          update: (_, b, a) => a..b=b
        ),
      ],
      child: MyApp(),
  ));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(context) {
    return MaterialApp(
      title: 'My Flutter App',
      home: Scaffold(
          body: Center(
              child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                      Text(
                          'Current selected Color',
                      ),
                      Consumer<D>(
                        builder: (context, d, _) => Placeholder(color: d.color)
                      ),
                  ],
              ),
          ),
          floatingActionButton: FloatingActionButton(
              onPressed: () => Provider.of<D>(context, listen: false).color = Colors.black,
              tooltip: 'Increment',
              child: Icon(Icons.arrow_forward),
          ),
      ),
    );
  }
}

このアプリは、あなたに基づい作品ABCおよびDクラス。

あなたの例Dでは、依存関係のないプロキシのみを使用しているため、プロキシは使用していません。しかし、この例では、プロバイダーが依存関係を正しくフックしていることがわかります。

Consumer<A>(
  builder: (context, a, _) => Text(a.b.c.d.runtimeType.toString())
),

「D」を印刷します。

ChangeColor()を呼び出していないため、機能しませんでしたnotifyListeners()

この上にステートフルウィジェットを使用する必要はありません。

4
Abion47 2019-12-19 00:06.

前に述べたように、設定は非常に複雑に見えます。モデルクラスのすべてのインスタンスはであり、ChangeNotifierしたがってそれ自体を維持する責任があります。これはアーキテクチャ上の問題であり、将来的にはスケーリングとメンテナンスの問題につながる可能性があります。

存在するほぼすべてのソフトウェアアーキテクチャには、共通点があります。状態をコントローラから分離します。データは単なるデータでなければなりません。プログラムの残りの部分の操作に関係する必要はありません。一方、コントローラー(ブロック、ビューモデル、マネージャー、サービス、または呼び出したいもの)は、プログラムの残りの部分がデータにアクセスまたは変更するためのインターフェイスを提供します。このようにして、関心の分離を維持し、サービス間の相互作用のポイントの数を減らして、依存関係の関係を大幅に減らします(これは、プログラムをシンプルで保守しやすくするのに大いに役立ちます)。

この場合、適切なのは不変状態アプローチである可能性があります。このアプローチでは、モデルクラスはまさにそれです-不変です。モデル内の何かを変更したい場合は、フィールドを更新する代わりに、モデルクラスインスタンス全体をスワップアウトします。これは無駄に思えるかもしれませんが、実際には、設計により状態管理にいくつかのプロパティが作成されます。

  1. フィールドを直接変更する機能がない場合、モデルのコンシューマーは、代わりにコントローラーで更新エンドポイントを使用する必要があります。
  2. 各モデルクラスは、プログラムの残りの部分でのリファクタリングの量が影響を与えないという自己完結型の信頼できる情報源になり、過剰結合による副作用を排除します。
  3. 各インスタンスは、プログラムが存在するためのまったく新しい状態を表します。したがって、適切なリスニングメカニズム(ここではプロバイダーで実現)を使用すると、状態の変化に基づいて更新するようにプログラムに指示するのは非常に簡単です。

モデルクラスが不変の状態管理によってどのように表されるかの例を次に示します。

main() {
  runApp(
    ChangeNotifierProvider(
      create: FleetManager(),
      child: MyApp(),
    ),
  );
}

...

class FleetManager extends ChangeNotifier {
  final _fleet = <String, Aircraft>{};
  Map<String, Aircraft> get fleet => Map.unmodifiable(_fleet);

  void updateAircraft(String id, Aircraft aircraft) {
    _fleet[id] = aircraft;
    notifyListeners();
  }

  void removeAircraft(String id) {
    _fleet.remove(id);
    notifyListeners();
  }
}

class Aircraft {
  Aircraft({
    this.aircraftManufacturer,
    this.emptyWeight,
    this.length,
    this.seats = const {},
    this.crewMembers = const {},
  });

  final String aircraftManufacturer;
  final double emptyWeight;
  final double length;
  final Map<int, Seat> seats;
  final Map<int, CrewMember> crewMembers;

  Aircraft copyWith({
    String aircraftManufacturer,
    double emptyWeight,
    double length,
    Map<int, Seat> seats,
    Map<int, CrewMember> crewMembers,
  }) => Aircraft(
    aircraftManufacturer: aircraftManufacturer ?? this.aircraftManufacturer,
    emptyWeight: emptyWeight ?? this.emptyWeight,
    length: length ?? this.length,
    seats: seats ?? this.seats,
    crewMembers: crewMembers ?? this.crewMembers,
  );

  Aircraft withSeat(int id, Seat seat) {
    return Aircraft.copyWith(seats: {
      ...this.seats,
      id: seat,
    });
  }

  Aircraft withCrewMember(int id, CrewMember crewMember) {
    return Aircraft.copyWith(seats: {
      ...this.crewMembers,
      id: crewMember,
    });
  }
}

class CrewMember {
  CrewMember({
    this.firstName,
    this.lastName,
  });

  final String firstName;
  final String lastName;

  CrewMember copyWith({
    String firstName,
    String lastName,
  }) => CrewMember(
    firstName: firstName ?? this.firstName,
    lastName: lastName ?? this.lastName,
  );
}

class Seat {
  Seat({
    this.row,
    this.seatColor,
  });

  final int row;
  final Color seatColor;

  Seat copyWith({
    String row,
    String seatColor,
  }) => Seat(
    row: row ?? this.row,
    seatColor: seatColor ?? this.seatColor,
  );
}

航空機をフリートに追加、変更、または削除する場合は常にFleetManager、個々のモデルではなく、を通過します。たとえば、乗組員がいて、名前を変更したい場合は、次のようにします。

final oldCrewMember = oldAircraft.crewMembers[selectedCrewMemberId];
final newCrewMember = oldCrewMember.copyWith(firstName: 'Jane');
final newAircraft = oldAircraft.withCrewMember(selectedCrewMemberId, newCrewMember);
fleetManager.updateAircraft(aircraftId, newAircraft);

確かに、それは単なるよりも少し冗長ですcrewMember.firstName = 'Jane';が、ここでのアーキテクチャ上の利点を考慮してください。このアプローチでは、相互依存関係の大規模なウェブがありません。どこかでの変更が他の多くの場所に影響を与える可能性があり、その一部は意図的ではない可能性があります。状態は1つしかないため、何かが変わる可能性のある場所は1つだけです。この変更をリッスンしている他のすべてのものは通過FleetManagerする必要があるため、心配する必要のあるインターフェイスのポイントは1つだけです。つまり、潜在的に数十ではなく、1つの障害ポイントです。このすべてのアーキテクチャのセキュリティとシンプルさにより、コードの冗長性を少し高めることは価値のある取引です。

これは少し単純な例であり、それを改善する方法は確かにありますが、とにかくこの種のものを処理するパッケージがあります。不変の状態管理をより堅牢に実行するには、flutter_blocまたはreduxパッケージを確認することをお勧めします。reduxパッケージは基本的にReactto FlutterのReduxの直接ポートであるため、Reactの経験があれば、自宅にいるように感じるでしょう。flutter_blocパッケージは、不変の状態に対して少し規則性の低いアプローチを採用し、有限状態のマシンパターンも組み込んでいます。これにより、アプリが常にどの状態にあるかを判断する方法を取り巻く複雑さがさらに軽減されます。

(この例では、Manufacturer列挙型をAirlineクラスの文字列フィールドに変更したことにも注意してください。これは、世界中に非常に多くの航空会社が存在するため、それらすべてに対応するのが面倒になるためです。列挙型で表されていないメーカーは、フリートモデルに保存できません。文字列にすることで、積極的に維持する必要が1つ少なくなります。)

Related questions

MORE COOL STUFF

ケイト・ブランシェットは3日間一緒に夫と一緒に寝て、25年経ってもまだ夫と結婚しています

ケイト・ブランシェットは3日間一緒に夫と一緒に寝て、25年経ってもまだ夫と結婚しています

ケイト・ブランシェットは、夫に会ったとき、典型的な交際のアドバイスに逆らいました。

マイケルシーンが非営利の俳優である理由

マイケルシーンが非営利の俳優である理由

マイケルシーンは非営利の俳優ですが、それは正確にはどういう意味ですか?

ホールマークスターのコリンエッグレスフィールドがRomaDramaLiveでスリル満点のファンと出会う![エクスクルーシブ]

ホールマークスターのコリンエッグレスフィールドがRomaDramaLiveでスリル満点のファンと出会う![エクスクルーシブ]

特徴的なスターのコリン・エッグレスフィールドは、RomaDrama Liveでのスリル満点のファンとの出会いについて料理しました!加えて、大会での彼のINSPIREプログラム。

「たどりつけば」をオンラインでストリーミングできない理由

「たどりつけば」をオンラインでストリーミングできない理由

ノーザンエクスポージャーが90年代の最も人気のある番組の1つになった理由を確認するには、Blu-rayまたはDVDプレーヤーをほこりで払う必要があります。

バイオニック読書はあなたをより速く読むことができますか?

バイオニック読書はあなたをより速く読むことができますか?

BionicReadingアプリの人気が爆発的に高まっています。しかし、それは本当にあなたを速読術にすることができますか?

ドミニカのボイリング湖:アクセスは簡単ではありませんが、ハイキングする価値があります

ドミニカのボイリング湖:アクセスは簡単ではありませんが、ハイキングする価値があります

ドミニカのボイリング湖は、世界で2番目に大きいボイリング湖です。そこにたどり着くまでのトレッキングは大変で長いですが、努力する価値は十分にあります。

私たちの水をきれいに保つのを助けるためにあなたの髪を寄付してください

私たちの水をきれいに保つのを助けるためにあなたの髪を寄付してください

サロンからのヘアトリミングや個人的な寄付は、油流出を吸収して環境を保護するのに役立つマットとして再利用できます。

ホワイトハウスの最も記憶に残る結婚式を見てください

ホワイトハウスの最も記憶に残る結婚式を見てください

過去200年以上の間にホワイトハウスで結婚したのはほんの数人です。彼らは誰でしたか、そしてそこで結婚式を獲得するために何が必要ですか?

この太陽に優しいショルダーバッグで一日中外出してください

この太陽に優しいショルダーバッグで一日中外出してください

画像クレジット:Richard Mackney / Flickrトラベリングライトは必需品だけを運ぶことを意味するかもしれませんが、デバイスを補充する方法がない外出先では、接続を維持するのが難しくなる可能性があります。それはあなたがすべての生き物の快適さやクールなガジェットを捨てる必要があるという意味ではありません、ただあなたがいくつかのより小さなものを手に入れる必要があるということです、そしておそらくあなた自身をジュースに保つためにいくつかの、例えば非正統的な充電装置を使うでしょう。

ミッドセンチュリーリゾートのポストカードが廃墟に変わるのを見る

ミッドセンチュリーリゾートのポストカードが廃墟に変わるのを見る

ニューヨーク州スプリンググレンにある放棄されたホモワックロッジのボーリング場。キャッツキル南部のこの地域は、ニューヨーク市からのユダヤ人の行楽客に人気があることから、かつてはボルシチベルトとして知られていました。

ブルックリンスレートの美しいボードをあなたのテーブルに座らせましょう

ブルックリンスレートの美しいボードをあなたのテーブルに座らせましょう

ブルックリンスレートブルックリンスレートのマグカップとコースターの賞賛をすでに歌っており、それらの食器製品も同様に堅実です。ブルックリンスレートは、さまざまなサイズとテクスチャのスレートの完全な採石場を販売しています。一部のオプションは赤でも利用できます。上で見ることができるように、彼らは同様にカスタマイズをします。

遺伝子分析により、私たちの体内に生息する微生物の99%がカタログ化されていないことが明らかになりました

遺伝子分析により、私たちの体内に生息する微生物の99%がカタログ化されていないことが明らかになりました

画像:Juan Gaertner / Shutterstock私たちの体の内部は、私たちの細胞とは何の関係もない何十億もの微生物が住んでいる本物の生態系です。これがまだ少し気になることではなかったかのように、これらの微生物の99%が研究されたことがないことがわかりました。

Zendaya Wishes Boyfriend Tom Holland Happy Birthday with Cuddly Photo: He 'Makes Me the Happiest'

Zendaya Wishes Boyfriend Tom Holland Happy Birthday with Cuddly Photo: He 'Makes Me the Happiest'

Zendaya shared a sweet photo in honor of boyfriend Tom Holland's 26th birthday Wednesday

小さな女性:脳卒中を患った後に病院から解放されたアトランタのジューシーな赤ちゃん:「まだ癒し」

小さな女性:脳卒中を患った後に病院から解放されたアトランタのジューシーな赤ちゃん:「まだ癒し」

シーレン「Ms.JuicyBaby」ピアソンは、先月脳卒中で入院した後、「もう一度たくさんのことをする方法を学ばなければならない」ため、言語療法を受けていることを明らかにしました。

エマストーンは彼女のクリフサイドマリブビーチハウスを420万ドルでリストアップしています—中を見てください!

エマストーンは彼女のクリフサイドマリブビーチハウスを420万ドルでリストアップしています—中を見てください!

オスカー受賞者の世紀半ばの家には、3つのベッドルーム、2つのバス、オーシャンフロントの景色があります。

ジーニー・メイ・ジェンキンスは、母乳育児の経験の中で、彼女は「本当に、本当に落ち込んでいる」と言います

ジーニー・メイ・ジェンキンスは、母乳育児の経験の中で、彼女は「本当に、本当に落ち込んでいる」と言います

ジーニー・メイ・ジェンキンスは、生後4か月の娘、モナコに母乳育児をしていると語った。

投資ノート:Bioscout AU$300万シード

投資ノート:Bioscout AU$300万シード

Bioscoutは、農家を運転席に置くという使命を負っています。Artesian(GrainInnovate)やUniseedと並んで、最新のシードラウンドでチームを支援できることをうれしく思います。問題真菌症による重大な作物の損失は、農民にとって試練であることが証明されています。

リトルマーケットリサーチ1| 2022年のクイックグリンプス遠隔医療市場

リトルマーケットリサーチ1| 2022年のクイックグリンプス遠隔医療市場

遠隔医療は、パンデミック後の時代では新しいものではなく、時代遅れの分野でもありません。しかし、業界を詳しく見ると、需要と供給の強力な持続可能性と、米国で絶え間ない革命となる強力な潜在的成長曲線を示しています。

スタートアップ資金調達環境:タイのスタートアップエコシステムの次は何ですか?

スタートアップ資金調達環境:タイのスタートアップエコシステムの次は何ですか?

2021年は、世界的なベンチャーキャピタル(VC)の資金調達にとって記録的な年でした。DealStreetAsiaによると、東南アジアも例外ではなく、この地域では年間で記録的な25の新しいユニコーンが採掘されました。

ムーアの法則を超えて

ムーアの法則を超えて

計算に対する私たちの欲求とムーアの法則が提供できるものとの間には、指数関数的に増大するギャップがあります。私たちの文明は計算に基づいています—建築と想像力の現在の限界を超える技術を見つけなければなりません。

Language