Writing the new iFood for Partners — Part 2: Bloc?!

This is part two of a series about how we're rewriting, refactoring, and redesigning our app. Know more.

Hello! My name is Gildásio. I'm a software engineer on the iFood for Partners team, and this is the second part of a series about how we're rewriting, refactoring, and redesigning our app in preparation for a big new feature that will make it a Flutter super app.

The original version of this article was published in English, take a look there!


The first part
it was about how we handled the migration to Flutter 2.0, while planning a new architecture for our interface layer. Now, I'm going to go into more detail about what we had before, what we ended up doing, and where we are after working with this new architecture.

But what is a module?!

First of all, I should probably explain what this thing I've been calling a “module” is. A module in our application is a separate Dart package that has all the necessary layers divided as follows:

  • domain — entities, use cases and repository abstractions;
  • date — implementations of these abstractions;
  • ui — pages, views It is widgets; with blocks inside the folder of each page.

This way, we can manage the dependencies of each package that do not need to be used in other parts of the application, and control what exactly comes out of each package if it needs to be used in another.

This is one of the application modules, there is an example below using it!

In this article we will talk about the third layer, the user interface, and how we are integrating new state management while thinking about our own version to make it even easier to use.

What we had

As explained before, the application had two years of code based on an architecture that made our migration to Flutter 2.0 a little difficult, and although the layers of domain and data well organized, the UI was a mix of Provider + MVP + rxdart and it wasn't very friendly to work for someone who had just joined the team. Coming from two consecutive projects using the block, I suggested and decided with the team how we could make the work even easier, knowing that the new members did not yet have any experience with it.

O Business Logic Component (business rule component) can be difficult to understand coming from other types of architecture, such as MVC/MVP and even MVVM, which I consider to be the closest, so we thought “let's use the most basic version and reiterate over time ”. So although we are actually using the bloc library, we are not using the Bloc class yet. We are using Cubits!

We opted for simplicity by just having functions that change the state instead of depending on events, which could be another step for new members to try to understand, in addition to gaining more code for the event classes. We lost the traceability of a normal bloc, but as long as we are manually monitoring each state transition, we shouldn't have that much of a problem. For the names of each component/class/file, however, we decided to call them Blocs in case we need to “evolve” to a more complex page. From now on in the article, when I say “bloc”, I am referring to a Cubit (which is true, as they both come from the BlocBase).

What have we done

With the blocs in hand, we had to plan how exactly we would use them; what kind of states, error handling, and widgets would be part of this new architecture.

For the states, we decided to keep it simple by having Loading, Failure It is Success/Loaded for each block; and sometimes having the Initial variant for pages that need any type of input first. Therefore, any new block would have 3 basic states and would be free to be updated as needed.

For error handling, we decided to keep the use cases “free” to throw exceptions that would be caught by bloc and then converted to error states. Failure, which would then show a Snackbar or a full view based on the situation. For error messages based on what happened, since we are separating each action into a different function, we already have a way to differentiate each message. Just have one enum with all types of error for each block, and then add this enum to the state of Failure and, in BlocListener, change the enum for a String localized (we are using i18n!), since we decided not to have any code Flutter inside the blocks.

With feedback from our tech lead, who has more than 8 years of experience in Android development, we created a three-layer hierarchy for the screen management part: pages, views and widgets.

  • A page represents any Widget that has a navigation route for it; generally these are the widget classes that have a Scaffold and will have a separate page title in their AppBar. A page can only show one view at a time;
  • One view represents any relationship State-Widget; a state Loaded has a view to “render” it, a Failure may also have one, and the Loading it's just our custom loading indicator, but we're free to change it into something else. And then we have a view “empty” based on the result of the Loaded, if it is a list, then we just change the view to the variant Empty. One view how many can you have? widgets are necessary.
  • And then one widget is the lowest component of a page or view and is simply anything complex enough to earn its own separate class/file as a component. They must be free of dependencies, receive everything they need in their constructor and whoever is using it must take care of the integration with their bloc.

If this all sounds similar to what you would see in an Android app, that's because it is! It's basically the scheme Activity-Fragment-View, but stop Flutter and with some names changed. Not that this is complicated: we just create an additional step to link a state to something that constructs it as a Widget and we call it view.

Examples

The three states of our menu screen in the new architecture

Now with an example, let's look at the ChooseMenuPage above, which uses the ChooseMenuBloc.

As soon as you enter this page, the initial state of the bloc is ChooseMenuLoadingState, which renders what we call PomodoroLoading. Then the function bloc.getMenus() is called, and as we are already in the Loading, all it does is call the use case responsible for fetching the menu list, and redirect this result to the ChooseMenuLoadedState, which turns the ChooseMenuLoadedView, in the middle image.

If anything goes wrong within the function, the ChooseMenuFailureState appears to save the day, rendering an error message and a button to try again, which will take the screen to the ChooseMenuLoadingState and restart the cycle.

And how are we?

If you were hoping for some greater conclusion on how to manage your screens, unfortunately this is not the article for you! We decided together on the best way to update the communication infrastructure between repositories and screens so that new team members could have everything as easy as possible in a new module.

We are very happy so far, having linked the state to a view made our lives easier and reduced the size of our pages, which now only concern us with knowing which views use, and when. Each view it can be as complex as you want since it's in another file, and we can test them separately too. We hope this article helped you think of another way to organize your interface layer in Flutter, and feel free to discuss our solution in the comments!

The next, and final, part of this series of articles will talk about our journey in integrating the current Order Manager, which only exists for Android today, within our iFood super app for Partners.

Was this content useful to you?
YesNo

Related posts