Refactoring a Flutter Project — a Story About Progression and Decisions
When doing any IT project, we sometimes don’t think on the weight of every decision we are making. Abstracting too much or too less, using X, Y or Z library, gluing up code that “will be fixed later”, and so many other countless examples. Each of these decisions has a cost. The peculiar thing is that that cost is going to bite us back not today, not tomorrow, but eventually, and when that “eventual” day comes, we may lose hours, days or weeks trying to retrace our steps or fixing our mess of a codebase.
This article is going to expose one of those stories. It’s not an article about how to create the “perfect architecture”, nor the best approaches for each problem, but it is an article about how I struggled with these issues and how I found a solution that, for the moment, works perfectly in my case.
Also, this also serves to show that it’s okay to make mistakes. When we are inexperienced, both in programming or in a specific framework or technology, we will make bad decisions eventually. That’s not what matters, what matters is how we handle them in the future.
So let’s begin our story.
The starting BLoCs of an App
When coming to Flutter and Dart, it might take a while to get into the flow of how asynchronous programming works here. Since there is no multi-threading in Dart (only the concept of Isolates, which you can read more about in this article from Didier Boelens: Futures — Isolates — Event Loop), we can only rely on
Sink s. But if we want to use Vanilla BLoC in our app, we have to go through the pains of learning them.
Thankfully, the concept of BLoC itself is deceptively simple: a BLoC is a helper class in which inputs are
Sinks and outputs are
Streams. We can choose to use RxDart (if we are comfortable with the concepts of RX) to make us of
BehaviorSubjects to expose the
Sink, leading us to create BLoCs such as the following example:
This simple BLoC’s purpose is to get input data from the user via
inputSink, add 1 to that value and then output data in the
outputStream. For this example and the rest of the article, we won’t comment on the implementation of this business logic or how we should approach it.
But this is just one BLoC, and for one screen only. Eventually, our app grows and we might see that we have recurrent functionalities that we are copying and pasting on each BLoC, such as relaying loading events or error messages. But before copying and pasting this code yet again, we must pause and think about that decision.
Effectively, copying and pasting our code is reusing the same functionality in different classes. However, let us imagine that we have over 20 BLoCs with pasted code. In this situation, the cost of wanting to change this functionality would be tremendous, since we would have to analyse each class, change the code, and guarantee that it didn’t break anything in each screen.
So, before proceeding we might want to find a better solution to manage this recurrent code in a way that if we have to change it, we change it in one place only.
One way we can approach this is to create a Base Class that holds all the major functionalities, but this might not fit every screen, because there’s a specific piece of code that only exists in 30% of your classes. For that, we have Mixins, which you can read more about in this article.
Yet again, we need to analyze our project and see what best fits our needs, since either abusing Base Classes or Mixins can eventually lead to more complications down the line. Why? Because we might feel tempted to add more and more code to these classes and mixins, and there will be a point where we create a new screen that requires part of those functionalities, but not all, and so we will have to refactor our code yet again.
Mixing Up the BLoCs — Where the Logic begins to crumble
Let us suppose that we have an app that has a list of all the produces we have in our house, divided by category: greens and meats. We want to be able to know how many products we have of each category, as well as checking and modifying the list of meats and vegetables that we have.
We can divide this app into 4 screens:
- A screen that lists types of ingredients: greens and meats — Categories Screen
- A screen with the list of produces for a category: eg.: greens should have a list with Lettuce, Beans, and Tomatoes — Item List Screen
- A screen to add new produce — Add Item Screen
- A screen to edit produce — Edit Item Screen
In this case, we may come up with the following flow:
- Categories Screen fetches data from the API and passes a filtered list only containing the correct category to the Item List Screen.
- When a user taps an item, the item is passed as an argument to the Edit Screen
- When a user taps the “Create New Item” button, a category id is passed as an argument to the Add Screen
As we can see from the schematics:
- In the
Category Screen, we communicate with the API, and pass a filtered list to the
- In the
List Screen, screen we then pass either the category id or the produce object to the
Edit Screen, respectively.
This clearly has a problem: when we are adding new data or editing data, how can we call the API again to fetch new data and display the updated data on the
List Screen screen?
One solution could rely on the BLoCs communicating with each other. On the upside, this would solve the problem, since the
Edit BLoC or
Add BLoC could send an event directly to the
Category BLoC. But is it the correct solution?
After thinking for a while, we may come with a couple of reasons not to use this approach:
- We may have some references problems if one of the BLoCs is recreated, for example, if the
Category BLoCis recreated for some reason after the
Add BLoCis initialised, then it will call methods on an object that's no longer in use.
- As Ivan Montiel writes in his Low Coupling, High Cohesion article: “If Module A knows too much about Module B, changes to the internals of Module B may break functionality in Module A.”
- This approach forces us to have dependencies between BLoCs and this may lead to a circular dependency, where
Add BLoCis a dependency of
Category Screenis also a dependency of
The alternatives? There are plenty, but since this article has been based on a true story, we’ll discuss the solution that was at the time chosen for this problem.
Sharing a Single Source of Truth
One thing that wasn’t discussed so far is how we are treating data.
At the moment, we retrieve data from the internet, pass it from screen to screen and mutate it. But, if we change our data by either adding or editing an item, the list in the
Category BLoC will not reflect those changes. This means that the list that we currently have in
Category BLoC and other BLoCs is different. If we can't fetch a new list from the
Category BLoC, then we will have two different lists inside the app containing different data, or in other words, different sources of truth.
On the other hand, we don’t want our BLoCs to be communicating with each other, but that does not mean that we can have the UI layer communicating with different BLoCs. So, we devise a new plan: we store the individual screen business logic in each screen’s BLoC, such as adding and editing, but we have a parallel BLoC that will hold the single source of truth for all data and whose task is to fetch data from the internet and update that list with new or updated data.
Having a single source of truth will also serve as a cache for our app. After adding or editing an item, if the user has either lost the internet connection or has slow internet connection, we can still show the most-up-to-date list before being able to receive new data from the server.
However, as we might see from the principle, we have a new problem: instead of having BLoCs communicating with each other, we have the UI communicating with multiple BLoCs at the same time and on the other hand we have multiple screens listening to the same stream, which means that we have to be careful in how we dispose and listen again for streams.
There might be other obvious flaws and positive points about our new approach, but before we analyze it ourselves, we are going to use one of Flutter’s main strengths: the community.
An Architecture on Feedback
Flutter’s community is amazing, from the Discord and Slack servers to Twitter where we can quickly ask for an opinion or feedback on a specific topic or if we are feeling brave, sharing our code and asking for an honest opinion.
Or, if we’re lucky, we can ask a good friend for an honest opinion. A good friend like Antonello Galipò.
After listening to our problem and our latest plans for the project, he gives a different perspective that changes how we should approach it:
My idea for a bloc is that it holds all the business logic for a feature (not a screen), which can be distributed among different screens. (…) You have the “type management” feature. You fetch them, see them, edit them, save them.
As with before, let’s discuss the benefits and cons of using this approach.
For possible cons, we may have:
- We give up on using a BLoC per screen and we now have a BLoC that’s being shared through every screen of a feature. This means, as before if we have a stream that is listened by multiple screens, we must be careful in how we dispose of the BLoC and re-listen to the stream when coming back to the screen.
- We have to pay attention to when and how we are creating a new instance of this BLoC, since if a new BLoC is generated when we are using this feature, all data will be lost.
As for pros for this approach:
- Using one BLoC per feature means that we only have a single source of truth, so all screens present the same data.
- Having one BLoC is easier to manage than having multiple BLoCs, since we just need to either provide it or pass it as an argument to subsequent screens when navigating. This also means that instead of having to dispose of each BLoC on each screen, now we just dispose our BLoC if we navigate away from this feature, leaving the responsibility to one screen only.
- Since all screens have access to this BLoC, when updating or adding items, we can directly call the
fetchmethod to update our current list.
- Using one BLoC means that we don’t need to use
initStateto add data to the
Sinks of each BLoC. If we don't need to use either the
disposemethods this also means that we can convert our widgets to
As stated before, the purpose of this article is not to find the perfect or correct solution to this problem. One week, two weeks from now, a new library, framework or even a simple idea can prompt us to reinvent our whole structure again, for better or worse.
What is the purpose of this article is to show you that it’s okay to have doubts about the decisions that we make when programming. We are continually learning either by creating new code or by discussing it with our colleagues, friends, and community. We have to be able to accept the feedback and let go of our ideas if they don’t provide the best solution to our problem. However, on the other hand, we must also be critical of each new solution that is published every week, else we’ll be changing into Redux, provider, Mobx or any other new state management framework each time a new update is released.
Flutter has an amazing community that thrives on sharing and helping others. Since we have this unique opportunity, we must use it to learn, grow and teach. We must accept that we sometimes do make mistakes and know when to ask for help, while at the same time being open to being helped.
Lastly, I hope that this article serves as a cautionary tale. We are constantly making decisions when coding, and sometimes we have that feeling of “I don’t think that this is the best way, but I’ll quickly change it in the near future”. But then comes a release. Then comes a new feature. Then comes a bug fix report. And then 4 months have passed and we don’t know why we used that specific logic, sometimes even with good documentation. And what would be 30 minutes to one hour, in the beginning, turned into countless hours of bug fixing since we have so much code that depends on that “quick and dirty fix”. Let’s not do that. If we are going to make it, let’s make it right the first time.💪