Writing the new iFood for Partners — Part 2: To Bloc or not to Bloc?

Gildásio Filho
iFood Engineering
Published in
6 min readNov 4, 2021

Hello again, I’m Gildásio, a software engineer from the iFood for Partners app team and this is the second part of a series about how we're rewriting, refactoring and redesigning our app in preparation of a new big feature that will turn it into a super-app in Flutter.

The first part was about how we handled the migration to Flutter 2.0 while making sure the next module could be done with a new architecture for the UI part, be sure to read that too! Now, I'll be going more into the details about what we had before, what we ended up doing, and how we feel about that after working with it for a while.

What the Module?!

First things first, I should probably explain what's this thing that I've been calling "module" for a while. A module in our app is a separated Dart package that has all necessary layers separated as follows:

  • domain — entities, usecases and abstractions of repositories and data sources
  • data — implementations of those abstractions
  • ui — pages, views and widgets; with blocs as a part of a page folder

This way we can manage each package's own dependencies that do not need to be used in other parts of it, and control what exactly gets out for each package to interact with each other, if needed.

This is part of the module used as example in this article.

What we'll be talking about here is the third layer, the UI, and how we're integrating a new state management while thinking about our own solution to make it even easier to use.

What We Had

As explained before, the app had two years worth of code based on an architecture that made our migration to Flutter 2.0 a bit difficult, and while the domain and data parts were pretty organized, the UI was a mixture of Provider + MVP + rxdart and it was not very friendly to work with for someone just arriving at the team. Coming from two projects in a row using the bloc library, I vouched for that and decided with the team how we could make it even easier to work with, knowing that the new members didn't have any experience with it yet.

The Business Logic Component can be difficult to understand coming from other types of UI solutions like MVC/MVP and even MVVM, which I think is its closest cousin, so we thought "let's use the most basic version and iterate on it over time". So, while we're indeed using the bloc library, we're not using the Bloc class yet. We're using Cubits!

We opted for the simplicity in just having functions that alter the state instead of relying on events, which could be another step for the new members to try to understand, as well as having more code for the event classes. We do lose the traceability of a normal Bloc, but as long as we're logging the steps and results we should be fine. For the names of each component/class/file, though, we decided on calling them Blocs in case we need to actually "evolve" from a cubit in a more complex page if it happens. From now on in the article, when I say "bloc" I am actually referring to a "cubit" (which is true since they both extend from BlocBase).

What We Did

With the blocs in hand, we then had to plan how exactly we would use them; which kind of states, error handling, widget management and so on.

For states, we decided to keep it simple by having the Loading, Failure and Success/Loaded types for every bloc; sometimes having the Initial variant for pages that need any kind of input first. So any new bloc would be paired with 3 base states and then updated with new ones as needed.

For error handling, we decided on keeping the usecases "free" to throw exceptions that would be caught by the bloc, and then converted into Failure states, which then would show a Snackbar, or a full-height view based on the situation. For error messages based on what happened, since we're separating every single action in a different function, we basically have a per-function way to get messages! By simply having an enum with all types of error for that bloc, we can add that enum to the Failure state, and in the BlocListener parse the enum into a string (we're also using i18n!) since we decided on not having any Flutter classes inside our blocs.

Taking feedback from our tech lead that has over 8 years of Android development experience, we came up with a three-step hierarchy for the widget management part: pages, views and widgets.

  • A page represents any Widget that has a navigation route to it; usually those are the Scaffold-type widget classes and will have a separate page title on their AppBar. A page can only show one view at a time as their body;
  • A view represents any State-Widget relationship; a Loaded state has a view to "render" it, a Failure state can also have one, the Loading is just our custom loading indicator, but we're free to change it into something else. And then we have an "empty" view based on the result of the Loaded state, if it's a list, then we just change the view to the Empty variant. A view can have as many widgets as needed.
  • And then a widget is the lowest component of a page or view, and is simply anything complex enough to earn their own separate class/file as a component. They should be dependency-free, receive everything they need in their constructor and whoever's building it should handle the integration with their bloc.

If all of this seems similar to what you would see in an Android app, that's because it is! It's basically the Activity-Fragment-View arrangement, but for Flutter and with some names swapped. Not that we're doing anything complicated here: we just created an additional step to link a State with something that builds it as a Widget, then named it.

Example

The three main states for our UI architecture

Now, for a quick example, the ChooseMenuPage above. It also has the ChooseMenuBloc.

As soon as you enter this page, the initial state set in the super() constructor is the ChooseMenuLoadingState, which renders into what we call the PomodoroLoading. It then calls bloc.getMenus()inside the initState and since it's already in the Loading state, it'll only try to call the underlying usecase inside the bloc and then redirect the list of objects to the Loaded state.

What you’re seeing in the middle is the ChooseMenuLoadedState built into the ChooseMenuLoadedView.

If anything wrong happens, it's ChooseMenuFailureState time to shine, rendering an error page with a custom message and a try again button that retries the main action for this page, which will emit ChooseMenuLoadingState once again, and then ChooseMenuLoadedState if everything works fine!

So What?

If you were expecting some greater conclusion on how to manage your UI, this is not the article! We decided as a team on a friendlier way to communicate with our existing infrastructure of repositories and usecases so our new members would have the best experience joining with a brand-new module.

We've been happy so far, and having the state-view relationship as close as it is helps us make complex UI elements without worrying that the page itself is getting too complex, since all the view code is separated and the page only has to worry about building the correct view at a time. We hope this article helps you take a different look at how your team decides how to build your UI with Flutter and feel free to discuss about our approach in the comments!

The next and final part of this series will talk about the hardest challenge we had so far: integrating the existing Order Manager app, which is currently only available on Android today, inside our already big enough Flutter app.

Check the vacancies available on iFood, learn more about the selection process and join the team.

--

--