Dealing with Side-Effects in Flutter
Thanks to Lucas Reis for his excellent article on simple react patterns that was a major influence for this article.
I’ve been writing applications using component driven frameworks like React, Vue, and now Flutter for a few years now. Along the way, I’ve noticed that some patterns tend to repeat themselves. In this post, I’ll review the patterns which summarize most of the code I write regularly to handle side effects.
One thing to note is that each of these examples could be refactored to use FutureBuilder, but I’ll avoid that for the sake of aiding my explanations.
The Vanilla or Mixed Pattern
I am more proficient as a top-down developer: I start with the high-level concept of what I need to build. I apply areas of code reuse and abstraction as I identify the need for them rather than trying to build from the bottom up. I think of it as KISS (keep it simple stupid) when I can and DRY (don’t repeat yourself) when I must.
95% of the time when I start building a widget it’s with the Vanilla pattern where the logic and the view are colocated in the widget.
This is a vanilla Flutter widget that you might write after just reading the docs. Its one main benefit is that it is simple and self-contained. Plugged in anywhere in your application, it will fetch and render data.
On the flip-side, writing widgets in this manner can quickly become a cognitive burden. What happens when there is complex styling, derived state, or more advanced branching scenarios beyond the network? What about if I want to unit test the view without fetching the data every time or without mocking requests? It’s just not possible.
In this example, the logic and the view are intertwined in one widget, hence Mixed pattern. A better way to build components that are more testable and maintainable is to separate the logic from the view. That’s where the second pattern comes in.
The Container/View Pattern
The Container/View pattern is also known as the Container-Presentational, Smart-Dumb, or Screen-Component pattern. The core idea is that how things work (Container) is isolated from how things look (View).
It should be immediately apparent that there is very little variation between this code and the code from the Vanilla pattern. The main difference is the isolation created by having two separate widgets encapsulating the two separate concerns. The PostContainer widget handles the logic (fetching data and branching) while the Post widget handles the view.
Now that we’ve separated the logic from the view, testing and styling the view is trivial. We no longer need to worry about mocking. This pattern is also very easy to build upon: breaking down the view into several widgets as it becomes more complex is trivial. We could even extract the “if” logic that defines what to render into its own widget. That leads us to our next pattern which is a variant of the Container/View pattern.
Variation: The Container/Branch/View Pattern
Now the individual views are even more isolated, which extends our testing capabilities and development workflow. Deciding how much to break up the view should be done on a case by case basis, but the rule of thumb should always be KISS first. If a widget is no longer simple and straightforward, it is probably a good candidate for refactoring.
There is one area where this pattern falls short: when you’d like to reuse logic and give control over the view that is built to the user. **Let’s look at my personal favorite pattern for dealing with this scenario.
This pattern may look intimidating at first, but just like our previous examples, we are mostly relocating code. The important thing to note about this pattern is that our logic is now decoupled from our view and passed in dynamically to FuturePostBuilder. The widget takes in a builder function that will return the view when executed.
The inversion of control here is powerful. It is entirely possible that we’d want to reuse the logic, but display a different view in different areas in the application, and this pattern is perfect for that. We could also do interesting things, like inject dependencies as props that are used to drive the logic. This is similar to how FutureBuilder works when you pass it a Future.
Like the Container/View pattern, this pattern also has a branching based variation.
Variation: Branching Builder Props
Note that this variation closely resembles the Builder Prop pattern, but just like with the Container/View variation we now explicitly separate individual views into three separate builder prop functions. And that’s it.
What if the side effects are costly?
There are times when the logic provided by a builder prop leads to costly code. Assuming that our FuturePostBuilder or another builder prop is very expensive and is used in several areas of a page, we wouldn’t necessarily want a future spawned for each of those scenarios. How can we prevent this from happening?
The Provider Pattern
A powerful pattern in Flutter, the Provider pattern allows you to gather data in an InheritedWidget to be shared in multiple places. I will not go too in-depth into how to apply this pattern — there are several articles devoted to that already. I will mention the awesome provider package that simplifies the boilerplate code necessary for InheritedWidget dramatically.
Hopefully, this article has taught you some patterns to help better manage your Flutter applications. Regular widgets work well but separating logic from view will improve the cohesiveness, readability, and testability of your code. At times where reusing logic is important, use builder props.
Separation of concerns is key to any type of software development, and it’s important to know that this is possible while staying in the context of a widget. When that logic is expensive and should execute once, use a provider.
You can find the code used in this article here.