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 c
As I've expressed before, the setup you have seems overly complicated. Every instance of a model class is a ChangeNotifier
and is therefore responsible for maintaining itself. This is an architectural problem that is going to lead to scaling and maintenance issues down the line.
Just about every software architecture in existence has something in common - separate the state from the controller. Data should only just be data. It shouldn't need to concern itself with the operations of the rest of the program. Meanwhile, the controller (the bloc, the view model, the manager, the service, or whatever you want to call it) supplies the interface for the rest of the program to access or modify the data. In this way, we maintain a separation of concerns and reduce the number of points of interaction between services, thus greatly reducing relationships of dependency (which goes a long way toward keeping the program simple and maintainable).
In this case, a good fit might be the immutable state approach. In this approach, the model classes are just that - immutable. If you want to change something in a model, instead of updating a field, you swap out the entire model class instance. This might seem wasteful, but it actually creates several properties in your state management by design:
Here's an example of how your model classes might be represented by immutable state management:
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,
);
}
Whenever you want to add, modify, or remove an aircraft from the fleet, you go through the FleetManager
, not the individual models. For example, if I had a crewmember and I wanted to change their first name, I'd do it like this:
final oldCrewMember = oldAircraft.crewMembers[selectedCrewMemberId];
final newCrewMember = oldCrewMember.copyWith(firstName: 'Jane');
final newAircraft = oldAircraft.withCrewMember(selectedCrewMemberId, newCrewMember);
fleetManager.updateAircraft(aircraftId, newAircraft);
Sure, it's a bit more verbose than just crewMember.firstName = 'Jane';
, but consider the architectural benefits in play here. With this approach, we don't have a massive web of inter-dependencies, where a change anywhere could have repercussions in a ton other places, some of which may be unintentional. There is only one state, so there is only one place where something could possibly change. Anything else listening to this change has to go through FleetManager
, so there is only one point of interface to need to worry about - one point of failure as opposed to potentially dozens. With all this architectural security and simplicity, a bit more verbosity in the code is a worthwhile trade.
This is a bit of a simple example, and though there are definitely ways to improve it, there are packages to handle this sort of stuff for us anyway. For more robust executions of immutable state management, I'd recommend checking out the flutter_bloc or redux packages. The redux package is essentially a direct port of Redux in React to Flutter, so if you have React experience you'll feel right at home. The flutter_bloc package takes a slightly less regimented approach to immutable state and also incorporates the finite state machine pattern, which further reduces the complexities surrounding how to tell what state your app is in at any given time.
(Also note that in this example, I changed the Manufacturer
enum to just be a string field in the Airline
class. This is because there are so many airline manufacturers in the world that it is going to be a chore keeping up with them all, and any manufacturer that isn't represented by the enum cannot be stored in the fleet model. Having it be a string is just one less thing you need to actively maintain.)
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.