My Flutter app has the following general structure:
I'm using FireStore. The list is built with Stream<QuerySnapshots>
, and when the user taps an item, the app's router parses the route name (e.g. /contacts/123
), creates the respective DocumentReference
and forwards it to the detail screen's initializer, which then uses DocumentReference.get
to load the details, and DocumentReference.updateData
to save changes. Works beautifully – but is only a proof of concept.
Now I would like to hide FireStore and the remaining business logic behind a BLoC. This leads to some questions:
streamOfContact(contact) -> Stream<Contact>
) is forbidden, so how would I do that with Sinks? Or is there generally a different way to do this without breaking the BLoC pattern? I'm very new to all of this, so may very well be I'm overlooking something important.The tutorials I've found online only deal with root data (e.g. adding cart items to a cart, handling user authentication, ...), but I haven't seen an example showcasing nested data yet. Any help is highly appreciated!
A variation of this classical pattern has emerged from the Flutter community: BLoC. BLoC stands for Business Logic Components. The gist of BLoC is that everything in the app should be represented as a stream of events: Widgets submit events, and other widgets will respond.
So here, we can compare the StreamBuilder in Bloc with Consumer in Provider. The difference is that StreamBuilder listens to the stream and fetches the model on every change to rebuild the widget. But Consumer listens as soon as notifyListeners() executes inside the provider class.
Bloc is a good pattern that will be suitable for almost all types of apps. It helps improve the code's quality and makes handling states in the app much more manageable. It might be challenging for someone who is just beginning to use Flutter because it uses advanced techniques like Stream and Reactive Programming.
A Cubit is similar to Bloc but has no notion of events and relies on methods to emit new states. Every Cubit requires an initial state which will be the state of the Cubit before emit has been called. The current state of a Cubit can be accessed via the state getter.
1) Routing and navigation is in the responsibility of the UI layer. That means the UI layer must call Navigator.push[Named](...)
.
If it makes sense, you can move the logic that decides if the app should navigate to the detail screen:
// in the BLoC:
Stream<int> showContactDetail;
// in the UI layer:
bloc.showContactDetail.listen(_showContactDetail);
How you transfer the parameters to the detail route is totally up to you. You can use named routes, but it would be easier to transfer data with a builder:
void _showContactDetail(int contactId) {
Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) {
return ContactDetailPage(
contactId: contactId,
);
}));
}
2) One BLoC should be tied to a single screen, that means there should be a separate BLoC for the detail screen (or dialog/sidebar/...), and you would pass the id of your contact to the second BLoC as a constructor parameter, or using a StreamSink
, or using a simple setter method.
I would recommend you to use plain old methods instead of StreamSink
s for the BLoC inputs. Here is a recent discussion about the topic.
3) The question is not only how to update your contact from the detail screen, but also how the detail BLoC obtains the contact data (if you are only transferring the contact id).
The answer: Another application layer, what I would call the data layer, that is shared between all BLoCs. You can store the data in Firebase, a sqlite database or a simple Map<int, Contact>
.
The data layer would also propagate changes to the backend, and notify all BLoCs when the data has changed, probably using a Stream
.
The next question that would come up is where you put this data layer (e.g. a class called ContactService
):
ContactService
in your ContactListPage
, then pass it to the ContactDetailPage
in a constructor parameter (using a route builder, as explained above). No magic here. A side effect that you may not want is that the service will be discarded when the list page is popped.InheritedWidget
that is above the MaterialApp
, or at least above the Navigator
generated by the MaterialApp
(You can use the builder
of MaterialApp
to wrap the navigator with your own widgets). Putting it that high up in the tree ensures that all pages of your app can access it.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With