web-dev-qa-db-fra.com

Objets imbriqués du fournisseur Flutter

J'utilise le Provider Package pour gérer l'état dans mon application Flutter. Je rencontre des problèmes lorsque je commence à imbriquer mes objets.

Un exemple très simple: le parent A a un enfant de type B, qui a un enfant de type C, qui a un enfant de type D. En enfant D, je souhaite gérer un attribut de couleur. Exemple de code ci-dessous:

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

Maintenant, mon main.Dart (qui gère mon widget Placeholder()) contient les éléments suivants:

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

Ce qui précède montre que l'attribut de couleur du Placeholder Widget est défini par Classe D propriété de couleur (A -> B -> C -> D.color). Le code ci-dessus est extrêmement simplifié, mais il montre le problème que je rencontre.

Retour au point : comment attribuer la propriété color de child D à un widget, de sorte que lors de la mise à jour child D, il met également à jour automatiquement le widget (en utilisant notifyListeners(), et non setState()).

J'ai utilisé Stateless , Stateful , Provider.of et Consumer , tout ce qui me donne le même résultat. Juste pour répéter, les objets ne peuvent pas être découplés, ils doivent avoir des relations parent-enfant.


[~ # ~] modifier [~ # ~]

Exemple plus complexe:

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;
}

Le code ci-dessus est une version simplifiée d'un exemple du monde réel. Comme vous pouvez l'imaginer, le terrier du lapin peut aller de plus en plus profond. Donc, ce que je voulais dire par l'exemple A à D essayait de simplifier la convolution de la situation.

Disons par exemple que vous souhaitez afficher et/ou modifier le nom d'un membre d'équipage dans un widget. Dans l'application elle-même, vous sélectionnez généralement un Aircraft dans le Fleet (passé au widget par l'index List), puis sélectionnez un CrewMember dans le Aircraft (passé par la touche Map) puis afficher/modifier le Name de CrewMember.

À la fin, votre widget pourra voir le nom du membre d'équipage auquel vous faites référence en utilisant les clés Aircrafts et CrewMembers passées.

Je suis définitivement ouvert à une meilleure architecture et à de meilleurs designs.

3
JBM

EDIT: réponse à la question mise à jour, original ci-dessous

Ce que A, B, C et D représentaient dans votre question initiale n’était pas clair. Il s'avère que c'étaient des modèles .

Ma pensée actuelle est, enveloppez votre application avec MultiProvider/ProxyProvider pour fournir des services , pas des modèles.

Je ne sais pas comment vous chargez vos données (voire pas du tout) mais j'ai supposé un service qui récupère votre flotte de manière asynchrone. Si vos données sont chargées par des pièces/modèles via différents services (au lieu de tous en même temps), vous pouvez les ajouter au MultiProvider et les injecter dans les widgets appropriés lorsque vous avez besoin de charger plus de données.

L'exemple ci-dessous est entièrement fonctionnel. Par souci de simplicité, et puisque vous avez posé des questions sur la mise à jour de name à titre d'exemple, je n'ai fait que définir cette propriété 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);
  }

}

Exécutez l'application, attendez 2 secondes pour que les données se chargent, et vous devriez voir "John" qui est membre d'équipage avec id = 1 sur cette carte. Ensuite, appuyez deux fois sur le texte et il devrait être mis à jour en "Peter".

Comme vous pouvez le constater, j'utilise l'enregistrement de haut niveau des services (Provider.value(value: Service())) et l'enregistrement au niveau local des modèles (ChangeNotifierProvider<CrewMember>(create: ...)).

Je pense que cette architecture (avec un nombre raisonnable de modèles) devrait être réalisable.

En ce qui concerne le fournisseur de niveau local, je le trouve un peu verbeux, mais il pourrait y avoir des moyens de le raccourcir. De plus, avoir une bibliothèque de génération de code pour les modèles avec des setters pour notifier les changements serait génial.

(Avez-vous un arrière-plan C #? J'ai corrigé vos classes pour qu'elles soient conformes à la syntaxe de Dart.)

Faites-moi savoir si cela fonctionne pour vous.


Si vous souhaitez utiliser Provider, vous devrez créer le graphique de dépendances avec Provider.

(Vous pouvez choisir l'injection de constructeur, au lieu de l'injection de setter)

Cela marche:

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),
          ),
      ),
    );
  }
}

Cette application fonctionne en fonction de vos classes A, B, C et D.

Votre exemple n'utilise pas de proxy car il utilise uniquement D qui n'a pas de dépendances. Mais vous pouvez voir que le fournisseur a correctement connecté les dépendances avec cet exemple:

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

Il imprimera "D".

ChangeColor() n'a pas fonctionné car il n'appelle pas notifyListeners().

Il n'est pas nécessaire d'utiliser un widget avec état en plus de cela.

4
Frank Treacy

Comme je l'ai déjà dit, la configuration que vous avez semble trop compliquée. Chaque instance d'une classe de modèle est un ChangeNotifier et est donc responsable de sa maintenance. Il s'agit d'un problème architectural qui entraînera des problèmes de mise à l'échelle et de maintenance sur toute la ligne.

Presque toutes les architectures logicielles existantes ont quelque chose en commun: séparer l'état du contrôleur. Les données ne doivent être que des données. Il ne devrait pas avoir besoin de se préoccuper des opérations du reste du programme. Pendant ce temps, le contrôleur (le bloc, le modèle de vue, le gestionnaire, le service, ou tout ce que vous voulez appeler) fournit l'interface pour le reste du programme pour accéder ou modifier les données. De cette façon, nous maintenons une séparation des préoccupations et réduisons le nombre de points d'interaction entre les services, réduisant ainsi considérablement les relations de dépendance (ce qui contribue grandement à garder le programme simple et maintenable).

Dans ce cas, un bon ajustement pourrait être l'approche de l'état immuable. Dans cette approche, les classes modèles ne sont que cela - immuables. Si vous souhaitez modifier quelque chose dans un modèle, au lieu de mettre à jour un champ, vous échangez l'intégralité de l'instance de classe de modèle. Cela peut sembler inutile, mais cela crée en fait plusieurs propriétés dans votre gestion d'état par conception:

  1. Sans la possibilité de modifier les champs directement, les consommateurs du modèle sont obligés d'utiliser à la place des points de terminaison de mise à jour dans le contrôleur.
  2. Chaque classe de modèle devient une source autonome de vérité qu'aucune quantité de refactorisation dans le reste de votre programme n'affectera, éliminant les effets secondaires d'un couplage excessif.
  3. Chaque instance représente un état entièrement nouveau pour que votre programme existe, donc avec le mécanisme d'écoute approprié (réalisé ici avec le fournisseur), il est extrêmement simple de dire au programme de se mettre à jour en fonction d'un changement d'état.

Voici un exemple de la façon dont vos classes de modèle peuvent être représentées par une gestion d'état immuable:

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,
  );
}

Chaque fois que vous souhaitez ajouter, modifier ou supprimer un avion de la flotte, vous passez par FleetManager, pas par les modèles individuels. Par exemple, si j'avais un membre d'équipage et que je voulais changer son prénom, je le ferais comme ceci:

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

Bien sûr, c'est un peu plus bavard que juste crewMember.firstName = 'Jane';, mais considérez les avantages architecturaux en jeu ici. Avec cette approche, nous n'avons pas un réseau massif d'interdépendances, où un changement n'importe où pourrait avoir des répercussions dans une tonne d'autres endroits, dont certains peuvent être involontaires. Il n'y a qu'un seul état, donc il n'y a qu'un seul endroit où quelque chose pourrait éventuellement changer. Tout autre élément écoutant ce changement doit passer par FleetManager, il n'y a donc qu'un seul point d'interface dont il faut se soucier - un point de défaillance par opposition à potentiellement des dizaines. Avec toute cette sécurité et cette simplicité architecturales, un peu plus de verbosité dans le code est un échange qui en vaut la peine.

C'est un exemple simple, et bien qu'il existe certainement des moyens de l'améliorer, il existe de toute façon des packages pour gérer ce genre de choses pour nous. Pour des exécutions plus robustes de la gestion des états immuables, je vous recommande de consulter les packages flutter_bloc ou redux . Le paquet redux est essentiellement un portage direct de Redux dans React vers Flutter, donc si vous avez l'expérience React), vous vous sentirez comme chez vous. Le paquet flutter_bloc adopte une approche légèrement moins réglementée de l'état immuable et incorpore également le modèle de machine à états finis, ce qui réduit encore les complexités entourant la façon de savoir dans quel état se trouve votre application à un moment donné.

(Notez également que dans cet exemple, j'ai changé l'énumération Manufacturer pour qu'elle soit juste un champ de chaîne dans la classe Airline. C'est parce qu'il y a tellement de fabricants de compagnies aériennes dans le monde que cela va pour être une corvée de les suivre tous, et tout fabricant qui n'est pas représenté par l'énumération ne peut pas être stocké dans le modèle de flotte. Avoir une chaîne est juste une chose de moins que vous devez maintenir activement.)

4
Abion47