Reputation: 355
For example, I am trying to obtain data emitted for multiple streams at once, but 2 or more of these streams emit data of the same type, lets say a string.
My question is, is it possible to use MultiProvider
and use multiple StreamProvider
(or any provider, but I am interested in this case) of the same type while still being able to access the data emitted by each one of them?
A solution for this is using a StreamBuilder
when using common data types but I really like what the MultiProvider
offers in terms of cleaner code.
Example:
class MyScreen extends StatelessWidget {
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<String>(stream: Observable.just("stream1")),
StreamProvider<String>(stream: Observable.just("stream2")),
StreamProvider<String>(stream: Observable.just("stream3"))
],
child: Builder(
builder: (BuildContext context) {
AsyncSnapshot<String> snapshot =
Provider.of<AsyncSnapshot<String>>(context);
String data = snapshot.data;
return Text(data);
},
),
);
}
}
Upvotes: 12
Views: 17799
Reputation: 606
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
uid
s 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();
}
}
}
Upvotes: 0
Reputation: 276911
MultiProvider
or not doesn't change anything. If two providers share the same type, the deepest one overrides the value.
It's not possible to obtain the value from a provider that is not the closest ancestor for a given type.
If you need to access all of these values independently, each should have a unique type.
For example, instead of:
Provider<int>(
value: 42,
child: Provider<int>(
value: 84,
child: <something>
),
)
You can do:
class Root {
Root(this.value);
final int value;
}
class Leaf {
Leaf(this.value);
final int value;
}
Provider<Root>(
value: Root(42),
child: Provider<Leaf>(
value: Leaf(84),
child: <something>
),
)
This allows to obtain each value independently using:
Provider.of<Root>(context)
Provider.of<Leaf>(context);
Upvotes: 21