Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nested data and BLoCs in Flutter

Tags:

flutter

dart

My Flutter app has the following general structure:

  1. The initial screen shows a list of contact objects. The user can tap a contact, which brings up
  2. a detail screen that lets the user see the whole contact info, and modify any details temporarily. The user can either
  3. dismiss the screen without saving the changes, or
  4. tap a save button, which updates the contact permanently, and upon completion leads back to the initial screen.

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:

  1. Keeping business logic out of the UI, as far as I understand it, implies that I should stick to named routes, and have the detail screen somehow use the route details to retrieve the relevant contact data through the BLoC. Is that true, and what's the best solution for this?
  2. How can I subscribe to nested data using the BLoC? I want the detail screen to observe data changes, so that I can warn the user if the data becomes stale. Using functions on BLoCs (something like 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.
  3. Similar question: How would I update a particular contact?

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!

like image 605
ahaese Avatar asked Aug 19 '18 19:08

ahaese


People also ask

What are blocs in Flutter?

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.

What is the difference between provider and BLoC in Flutter?

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.

Why BLoC is used in Flutter?

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.

What is Cubit and BLoC in Flutter?

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 Answers

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 StreamSinks 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):

  • You can create the 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.
  • Using scoped_model, which is basically the same. Must also be inserted above the navigator to be accessible from both routes
  • A static variable/singleton
like image 158
boformer Avatar answered Oct 09 '22 09:10

boformer