Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.0k views
in Technique[技术] by (71.8m points)

dart - Flutter Provider Nested Objects

I'm using the Provider Package to manage state in my Flutter App. I am running into issues when I start nesting my objects.

A very simple example: Parent A has child of type B, which has child of type C, which has child of type D. In child D, I want to manage a color attribute. Code example below:

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

Now my main.dart (which is managing my Placeholder() widget) contains the following:

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

The above shows that the Placeholder Widget's color attribute is defined by Class D's color property (A -> B -> C -> D.color). The above code is extremely simplified, but it does show the issue I'm having.

Back to the point: how would I assign child D's color property to a widget, so that when updating child D's property, it also automatically updates the widget (using notifyListeners(), not setState()).

I've used Stateless, Stateful, Provider.of and Consumer, all which gives me the same result. Just to reiterate, the objects can't be decoupled, it has to have parent-child relationships.


EDIT

More complex example:

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

The above code is a simplified version of a real world example. As you can imagine the rabbit hole can go deeper and deeper. So, what I meant by the A through D example was trying to simplify the convolution of the situation.

Lets say for example you want to display and/or change a crew members' name in a widget. In the app itself you would typically select an Aircraft from the Fleet (passed to widget by List index), then select a CrewMember from the Aircraft (passed by Map key) and then display/change the Name of CrewMember.

In the end your widget will be able to see what Crew Member's name you are referring to by using the passed in Aircrafts index and CrewMembers key.

I'm definitely open to a better architecture and designs.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

EDIT: answer to the updated question, original below

It was not clear what A, B, C and D stood for in your original question. Turns out those were models.

My current thinking is, wrap your app with MultiProvider/ProxyProvider to provide services, not models.

Not sure how you are loading your data (if at all) but I assumed a service that asynchronously fetches your fleet. If your data is loaded by parts/models through different services (instead of all at once) you could add those to the MultiProvider and inject them in the appropriate widgets when you need to load more data.

The example below is fully functional. For the sake of simplicity, and since you asked about updating name as an example, I only made that property setter 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);
  }

}

Run the app, wait 2 seconds for data to load, and you should see "John" which is crew member with id=1 in that map. Then double-tap the text and it should update to "Peter".

As you can notice, I am using top-level registering of services (Provider.value(value: Service())), and local-level registering of models (ChangeNotifierProvider<CrewMember>(create: ...)).

I think this architecture (with a reasonable amount of models) should be feasible.

Regarding the local-level provider, I find it a bit verbose, but there might be ways to make it shorter. Also, having some code generation library for models with setters to notify changes would be awesome.

(Do you have a C# background? I fixed your classes to be in line with Dart syntax.)

Let me know if this works for you.


If you want to use Provider you'll have to build the dependency graph with Provider.

(You could choose constructor injection, instead of setter injection)

This works:

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

This app works based on your A, B, C and D classes.

Your example does not use proxies as it only uses D which has no dependencies. But you can see Provider has hooked up dependencies correctly with this example:

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

It will print out "D".

ChangeColor() did not work because it is not calling notifyListeners().

There is no need to use a stateful widget on top of this.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...