Skip to content

Instantly share code, notes, and snippets.

@Vanethos

Vanethos/blog.md Secret

Created September 26, 2019 05:44
Show Gist options
  • Save Vanethos/77368f30048018678b24093159d54998 to your computer and use it in GitHub Desktop.
Save Vanethos/77368f30048018678b24093159d54998 to your computer and use it in GitHub Desktop.

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 cost is going to bite us back not today, not tomorrow, but eventually, and when that eventual day comes, we may loose 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[INSERT LINK]), we can only rely on Futures, Streams and Sinks. 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 Streams and outputs are Streams. We can also choose to use RxDart (if we are comfortable with the concepts of RX) and use PublishSubjects and BehaviorSubjects to expose the Stream and Sink, leading us to create BLoCs such as the following example:

https://gist.github.com/51f074c0ffabaf210ee88990296f3405

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.

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. And we stop to think.

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. 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 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 must yet again refactor our code.

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 in 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 a new produce - Add Item Screen
  • A screen to edit a 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 as argument to the Add Screen

This can be roughly translated in the following BLoCs:

https://gist.github.com/a96057ab6fb3e0bf3b0180edf3ec0a8c


Colocar gráfico em que se mostra como é que app está estruturada, e com que parte é que cada bloc comunica

Category - comunica para tirar a lista da API List - não comunica com API Add/Edit - comunica com API

As we can see from the schematics:

  • In the Category Screen we communicate with the API, and pass a filtered list to the List Screen.
  • In the List Screen screen we then pass either the category id or the produce object to the Add Screen or 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?

What if the BLoCs could communicate between 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 BLoC is recreated for some reason after the Add BLoC is 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 BLoC is a dependency of Category Screen but Category Screen is also a dependency of Add BLoC.

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 on 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.

image

With this, we can call the fetch command from any screen and since we only have one list, when we are adding or editing an item, this BLoC can have methods to update the current list. This is specially important if the user has a slow internet connection or if he suddenly has no internet connection, since we can show up-to-date information.

https://gist.github.com/05b9c85913d2fccbcda7f64b06348695

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 at the same time multiple screens listening to the same stream, which means that we have to be careful in how we dispose and listen again for streams.

And before we start engineering a new solution, 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 servers, to Slack or even Twitter. 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 sharing this information, he gives us a different perspective on how we should approach the problem:

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:

  • 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 the BLoC and re-listen to the stream when coming back to the screen.
  • We have to pay attention in 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.
  • However, 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 each BLoC in 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 fetch method to update our current list.
  • Using one BLoC means that we don’t need to use initState to add data to the Sinks of each BLoC. If we don’t need to use either the initState or dispose methods this also means that we can convert our widgets to StatelessWidgets.

We can now proceed to the implementation of this BLoC.

https://gist.github.com/02ec31b0747e9b11a256014f3476dd9e

Conclusion

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 in the decisions that we make when programming. We are continuing learning either by creating new code or by discussing with our colleagues, friends and community. We have to be able to take new 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.

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 hours 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment