Ali Amin
Ali Amin

Reputation: 355

Flutter - How does MultiProvider work with providers of the same type?

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

Answers (2)

1housand
1housand

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

Upvotes: 0

R&#233;mi Rousselet
R&#233;mi Rousselet

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

Related Questions