UPDATE: This solution I recently found seems to be cleaner and working better. The solution below is another way but requires more coding.
I was looking for a similar solution and couldn't find anything so I implemented my own with the MultiProvider, StreamGroup, and a ChangeNotifier.
I use the StreamGroup to hold all the streams I need to keep track of by adding and removing streams.
I didn't want to use a bunch of extra libraries and/or plugins.
In the ChangeNotifierProxyProvider, it runs the update function whenever the Family stream gets an update from StreamProvider<Family> above it.
// main.dart
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<Family>(
initialData: Family(),
create: (context) => FirebaseFireStoreService().streamFamilyInfo(),
),
ChangeNotifierProxyProvider<Family, FamilyStore>(
create: (context) => FamilyStore(),
update: (context, family, previousFamilyStore) {
// Manually calling the function to update the
// FamilyStore store with the new Family
previousFamilyStore!.updateFamily(family);
return previousFamilyStore;
},
)
],
builder: (context, child) => MaterialApp(),
);
}
The Family just holds an array of AdultProfile uids so I can keep track of the adults in a family. It's basically just a stream receiver.
// family.dart
class Family {
List<String> adults;
Family({
this.adults = const [],
});
factory Family.fromMap(Map<String, dynamic>? data) {
if (data == null) {
return Family(adults: []);
}
return Family(
adults: [...data['adults']],
);
}
}
In my firestore class, I have the necessary functions that return a Stream for the class I need. I only pasted the function for the Family, but same code for AdultProfile with minor path changes.
// firebase_firestore.dart
Stream<Family> streamFamilyInfo() {
try {
return familyInfo(FirebaseAuthService().currentUser!.uid).snapshots().map(
(snapshot) {
return Family.fromMap(snapshot.data() as Map<String, dynamic>);
},
);
} catch (e) {
FirebaseAuthService().signOut();
rethrow;
}
}
This is where most of the work happens:
// family_store.dart
class FamilyStore extends ChangeNotifier {
List<AdultProfile>? adults = [];
StreamGroup? streamGroup = StreamGroup();
FamilyStore() {
// Handle the stream as they come in
streamGroup!.stream.listen((event) {
_handleStream(event);
});
}
void handleStream(streamEvent) {
// Deal with the stream as they come in
// for the different instances of the class
// you may have in the data structure
if (streamEvent is AdultProfile) {
int index = adults!.indexWhere((element) => element.uid == streamEvent.uid);
if (index >= 0) {
adults![index] = streamEvent;
} else {
adults!.add(streamEvent);
}
notifyListeners();
}
// This is the function called from the
// ChangeNotifierProxyProvider in main.dart
void updateFamily(Family newFamily) {
_updateAdults(newFamily.adults);
}
void _updateAdults(List<String> newAdults) async {
if (newAdults.isEmpty) return;
// Generate list of comparisons so you can add/remove
// streams from StreamGroup and the array of classes
Map<String, List<String>> updateLists =
_createAddRemoveLists(adults!.map((profile) => profile.uid).toList(), newAdults);
for (String uid in updateLists['add']!) {
// Add the stream for the instance of the
// AdultProfile to the StreamGroup
(streamGroup!.add(
FirebaseFireStoreService().streamAdultProfile(uid),
));
}
for (String uid in updateLists['remove']!) {
// Remove the stream for the instance of the
// AdultProfile from the StreamGroup
streamGroup!.remove(
FirebaseFireStoreService().streamAdultProfile(uid),
);
// Also remove it from the array
adults!.removeWhere((element) => element.uid == uid);
notifyListeners();
}
}
}