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

Reba McEntire は、彼女が息子の Shelby Blackstock と共有する「楽しい」クリスマスの伝統を明らかにしました:「私たちはたくさん笑います」

Reba McEntire は、彼女が息子の Shelby Blackstock と共有する「楽しい」クリスマスの伝統を明らかにしました:「私たちはたくさん笑います」

Reba McEntire が息子の Shelby Blackstock と共有しているクリスマスの伝統について学びましょう。

メーガン・マークルは、自然な髪のスタイリングをめぐってマライア・キャリーと結ばれました

メーガン・マークルは、自然な髪のスタイリングをめぐってマライア・キャリーと結ばれました

メーガン・マークルとマライア・キャリーが自然な髪の上でどのように結合したかについて、メーガンの「アーキタイプ」ポッドキャストのエピソードで学びましょう.

ハリー王子は家族との関係を修復できるという「希望を持っている」:「彼は父親と兄弟を愛している」

ハリー王子は家族との関係を修復できるという「希望を持っている」:「彼は父親と兄弟を愛している」

ハリー王子が家族、特にチャールズ王とウィリアム王子との関係について望んでいると主張したある情報源を発見してください。

ワイノナ・ジャッドは、パニックに陥った休暇の瞬間に、彼女がジャッド家の家長であることを認識しました

ワイノナ・ジャッドは、パニックに陥った休暇の瞬間に、彼女がジャッド家の家長であることを認識しました

ワイノナ・ジャッドが、母親のナオミ・ジャッドが亡くなってから初めての感謝祭のお祝いを主催しているときに、彼女が今では家長であることをどのように認識したかを学びましょう.

セントヘレナのジェイコブのはしごを登るのは、気弱な人向けではありません

セントヘレナのジェイコブのはしごを登るのは、気弱な人向けではありません

セント ヘレナ島のジェイコブズ ラダーは 699 段の真っ直ぐ上る階段で、頂上に到達すると証明書が発行されるほどの難易度です。

The Secrets of Airline Travel Quiz

The Secrets of Airline Travel Quiz

Air travel is far more than getting from point A to point B safely. How much do you know about the million little details that go into flying on airplanes?

Where in the World Are You? Take our GeoGuesser Quiz

Where in the World Are You? Take our GeoGuesser Quiz

The world is a huge place, yet some GeoGuessr players know locations in mere seconds. Are you one of GeoGuessr's gifted elite? Take our quiz to find out!

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

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

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

米国が選挙に影響を与えるロシアのハックに関するインテリジェンスレポートを発表

米国が選挙に影響を与えるロシアのハックに関するインテリジェンスレポートを発表

写真:ゲッティイメージズ。「最近の米国の選挙に関するロシアの活動と意図の評価」に関するFBI、CIA、およびNSAからの機密解除された文書は、「ロシアのウラジーミル・プーチン大統領が米国大統領選挙に影響を与えるキャンペーンを命じた」と主張している。

La。Manは45年間で収集された500,000ペニーで現金化

La。Manは45年間で収集された500,000ペニーで現金化

ペニーは、2006年7月6日、イリノイ州グレンビューのグレンビューコイン&コレクティブルズに展示されています。合計5,000ドル以上で、News-Starは報告します。

SaartjieBaartmanと黒人女性の身体の所有権について

SaartjieBaartmanと黒人女性の身体の所有権について

19世紀のフランスの版画サラ・バートマンのラベル・ホッテントットウィキメディア・コモンズ黒人女性の体の所有権の認識は、最も醜い歴史の断片に深く織り込まれている問題です。この有毒な考えは、地球の一部に隔離されていません。

マライア・キャリーが大晦日のパフォーマンスでソーシャルメディアのメルトダウンに対応:「ShitHappens」

マライア・キャリーが大晦日のパフォーマンスでソーシャルメディアのメルトダウンに対応:「ShitHappens」

写真:APマライアキャリーは、土曜日の夜にタイムズスクエアでディッククラークスの新年のロッキンイブウィズライアンシークレスト(このショーのタイトルはサウンドチェックを取得できますか?)のパフォーマンス中に、かなり深刻なオーディオの誤動作に苦しんでいました。最悪の問題は、キャリーの「エモーション」のパフォーマンス中に発生しました。トラックがキャリーのボーカルを断続的にしか再生せず、リップシンクをオフにしました。

米国のフィギュア スケートは、チーム イベントでの最終決定の欠如に「苛立ち」、公正な裁定を求める

米国のフィギュア スケートは、チーム イベントでの最終決定の欠如に「苛立ち」、公正な裁定を求める

ロシアのフィギュアスケーター、カミラ・バリエバが関与したドーピング事件が整理されているため、チームは2022年北京冬季オリンピックで獲得したメダルを待っています。

Amazonの買い物客は、わずか10ドルのシルクの枕カバーのおかげで、「甘やかされた赤ちゃんのように」眠れると言っています

Amazonの買い物客は、わずか10ドルのシルクの枕カバーのおかげで、「甘やかされた赤ちゃんのように」眠れると言っています

何千人ものAmazonの買い物客がMulberry Silk Pillowcaseを推奨しており、現在販売中. シルクの枕カバーにはいくつかの色があり、髪を柔らかく肌を透明に保ちます。Amazonで最大46%オフになっている間にシルクの枕カバーを購入してください

パデュー大学の教授が覚醒剤を扱った疑いで逮捕され、女性に性的好意を抱かせる

パデュー大学の教授が覚醒剤を扱った疑いで逮捕され、女性に性的好意を抱かせる

ラファイエット警察署は、「不審な男性が女性に近づいた」という複数の苦情を受けて、12 月にパデュー大学の教授の捜査を開始しました。

コンセプト ドリフト: AI にとって世界の変化は速すぎる

コンセプト ドリフト: AI にとって世界の変化は速すぎる

私たちの周りの世界と同じように、言語は常に変化しています。以前の時代では、言語の変化は数年または数十年にわたって発生していましたが、現在では数日または数時間で変化する可能性があります。

SF攻撃で91歳のアジア人女性が殴られ、コンクリートに叩きつけられた

犯罪擁護派のオークランドが暴力犯罪者のロミオ・ロレンゾ・パーハムを釈放

SF攻撃で91歳のアジア人女性が殴られ、コンクリートに叩きつけられた

認知症を患っている 91 歳のアジア人女性が最近、47 番街のアウター サンセット地区でロメオ ロレンゾ パーハムに襲われました。伝えられるところによると、被害者はサンフランシスコの通りを歩いていたところ、容疑者に近づき、攻撃を受け、暴行を受けました。

ℝ

“And a river went out of Eden to water the garden, and from thence it was parted and became into four heads” Genesis 2:10. ? The heart is located in the middle of the thoracic cavity, pointing eastward.

メリック・ガーランドはアメリカに失敗しましたか?

バイデン大統領の任期の半分以上です。メリック・ガーランドは何を待っていますか?

メリック・ガーランドはアメリカに失敗しましたか?

人々にチャンスを与えることは、人生で少し遅すぎると私は信じています。寛大に。

Language