web-dev-qa-db-ja.com

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

Flutterアプリで状態を管理するために Provider Package を使用しています。オブジェクトのネストを開始すると、問題が発生します。

非常に単純な例:親AにはタイプBの子があり、タイプ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.Dart(これはPlaceholder()ウィジェットを管理しています)には以下が含まれています:

_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();
    }
}

_

上記は、Placeholder Widgetのカラー属性がClass Dのカラープロパティ_(A -> B -> C -> D.color)_。上記のコードは非常に単純化されていますが、私が抱えている問題を示しています。

ポイントに戻るchild Dのcolorプロパティをウィジェットにどのように割り当てますか? 子Dのプロパティを更新すると、ウィジェットも自動的に更新されます(notifyListeners()ではなくsetState()を使用)。

私はStatelessStatefulProvider.ofおよびConsumerすべて同じ結果が得られます。繰り返しますが、オブジェクトは分離できません。オブジェクトには親子関係が必要です。


[〜#〜]編集[〜#〜]

より複雑な例:

_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キーによって渡されます)、NameCrewMemberを表示/変更します。

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

私は間違いなく、より良いアーキテクチャとデザインにオープンです。

2
JBM

編集:更新された質問への回答、以下のオリジナル

元の質問でABCDが何を示しているのか明確ではありませんでした。それらがモデルであることがわかりました。

私の現在の考えは、アプリを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()は機能しませんでした。

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

1
Frank Treacy

以前に述べたように、あなたが持っているセットアップは非常に複雑に見えます。モデルクラスのすべてのインスタンスは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';よりも冗長ですが、ここで役立つアーキテクチャ上のメリットを考慮してください。このアプローチでは、相互依存関係の大規模なWebはありません。そのため、どこかで変更を加えると、他の多くの場所で波及が起こり、その一部は意図的ではない場合があります。状態は1つしかないため、何かが変化する可能性のある場所は1つだけです。この変更をリッスンする他のすべてはFleetManagerを経由する必要があるため、心配する必要があるインターフェイスのポイントは1つだけです。このすべてのアーキテクチャ上のセキュリティとシンプルさにより、コードの冗長性はもう少し価値があります。

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

(また、この例では、Manufacturer enumをAirlineクラスの文字列フィールドに変更したことに注意してください。これは、世界中に多くの航空会社が存在するためです。列挙型で表されていないメーカーは、フリートモデルに格納できません。文字列であることは、アクティブに維持する必要があることの1つ少ないだけです。)

3
Abion47