Sealed Classes appear in many modern programming languages like C#, Scala, or Kotlin. In general, they allow you to create restricted class hierarchies. As a result, Sealed Classes work very well in modeling the state of your app. They make your code more reliable and less error-prone.
To start with, Sealed Classes seem to be very similar to Enum types, but they also have some additional functionalities. The main difference is that Enums can have only one instance per type. On the other hand, Sealed Classes allow you to create several instances of the same class. What’s more, they may also contain a state, so every type can have its properties and methods.
The true power of pattern matching
Sealed Classes shine when you use them together with pattern matching and the “is” operator.
Just as a reminder, pattern matching is a mechanism for checking a value against a given pattern. You can consider it as a more powerful version of the switch statement from C-like languages. You can also use it to replace a series of if/else statements.
If you have attended an object-oriented programming course, you are probably anxious now. In general, switch statements create a knot of dependencies and are considered to be not object-oriented.
Usually, when we see a switch statement we should think of polymorphism. Polymorphism protects clients from new types. If you have to add a new execution variant, all you need to do is add a new subclass without touching the existing code.
Besides code for a switch often can be scattered in different places in the application. Therefore, when a new condition is added, you have to find all the switch cases and modify them. This process may be tedious and error-prone. What if you miss a place or two? The compiler will not complain about it. In the worst-case scenario, the program will fail during the runtime.
Sealed Classesprovide the best of two worlds: the freedom of representation of polymorphism and the restricted set of types of
When you combine Sealed Classes with pattern matching the compiler will notify you whenever a branch is uncovered. This means, that you don’t have to rely on your memory anymore when you create a new subtype!
Let the code begin
Our boss has ordered us to create an app that displays currency rates. We will use the National Bank of Poland REST API to fetch all needed data.
The app uses the BLoC pattern for state management, but all concepts from this article apply to Provider or Redux architecture as well. I will not explain in great detail the state management in Flutter, but if you are interested in this, you can find more info here.
BLoCpattern helps to separate presentation from business rules.
From a high-level perspective, a BLoC is just a component that takes a stream of events and converts them into states. Thanks to this, the presentation layer only delegates user events to the BLoC and observes the state changes.
Events usually represent user interactions or lifecycle events like page loads.
At the beginning the application will have only one event:
CurrencyRatesLoadEvent. This event informs the BLoC that it should load currency rates from the repository.
States represent the state of the application. The UI components should redraw themselves based on the current state of a BLoC.
For the time being, the application can be in one of the following states:
CurrencyRatesLoading- while the BLoC is fetching currency rates from the repository,
CurrencyRatesLoaded- when the BloC has fetched currency rates successfully and they can be presented to the user,
CurrencyRatesError- when the BLoC was unable to fetch the currency rates because of an error.
BLoC simply transforms
CurrencyRateState. It also uses the CurrencyRateRepository to retrieve the data.
The UI layer listens to state changes and renders accordingly. It displays progress indicator, list of currency rates, or an error depending on the BLoC’s state.
You have probably noticed a special case. Even though we know that the BLoC can emit only one of the following states:
CurrencyRatesError, the compiler is unaware of this. Thus, if we skipped the else clause, the compiler will generate a warning: This function has a return type of ‘Widget’, but doesn’t end with a return statement.
As a result, we have to provide the default case, even though it should never happen.
If something should never happen it will happen. And it will happen in the production.
So, how to handle this default case? Unfortunately, there’s no good way of doing that. We can, for example, return a placeholder widget like SizedBox, but this is like sweeping trash under the carpet. What’s more, when this error happens on a user’s device we will be unaware of it.
Thus, I suggest that, in this case, it’s slightly better to crash the application than let it remain in some unpredictable state.
Luckily, when using Sealed Classes we don’t have to do any of this. The compiler will make sure that the application is always in a legal state!
Looks like we are done. We can proudly present the Currency Rates app to our boss. He is very happy with our work and we may expect a big raise!
Change is the only constant thing in software development.
Changes, changes everywhere
The boss fully understands the immense potential the Currency Rates app has. As a result, new and exciting features are coming!
The management has realized that currency rates may change within seconds. For that reason they want the application to be more responsive and display the most recent data. Therefore, every 15 seconds we should update currency rates and present them to the user.
Unfortunately, the API we are using doesn’t provide live currency rates, thus we will mock this behavior by randomizing returned values.
Let us start with creating a new event that will inform the BLoC that a refresh is needed:
Then, we will add a periodic timer that emits this event every 15 seconds after the initial fetch has succeeded:
Next, the BLoC needs to handle this new
CurrencyRatesRefreshEvent and fetch the newest rates from the repository:
Furthermore, we have to introduce a
CurrencyRatesRefreshing state. This state can be only emitted when the rates have already been loaded. In response to this state, the UI should render the currency rates along with a progress indicator on top:
It seems that we are good to go, right? Let’s then launch our new, responsive app, and see how the currency rates change in real-time!
What has just happened!? The application doesn’t seem to work! The boss is really angry because he has already calculated how much money he will earn on this new Currency Rates app. He wants to know immediately what went wrong and why.
It seems, that we have forgotten to update a function that maps the state into an appropriate widget. Unfortunately, we were unaware of that, until the app had entered this unexpected state.
Importance of exhaustive states
You may think that I’m exaggerating and this defect was very easy to spot. But imagine that, this is a much bigger app. With many more components which do something in response to a change of the
CurrencyRatesState. In this case, how likely is it that you will forget to add desired behavior in one single place? Especially, on a screen that is hardly accessible and not visited very often by users. In conclusion, you may accidentally create a bug that will live for a very long time before it is discovered.
The later the bug is discovered the higher the cost of fixing it is.
Cost of fixing a bug
Think of, how much time does it take you to fix a bug? Well, it depends on how soon it will be discovered. When a bug slips through the compiler and unit tests you can be in trouble! Why? Because from this point the cost of fixing it increases tremendously. The diagram below illustrates it:
When a compiler discovers a defect, it usually takes you a couple of seconds to make your code work again.
Also, unit tests can give you almost instant information if your code works or not. You just lunch your suite of tests and within minutes or even seconds, you get feedback whether your code works as expected.
When a tester discovers unexpected behavior, he needs to make sure that it’s a defect. Then he creates a ticket in a bug tracking tool. Also, he provides a detailed description of how to reproduce this bug. Furthermore, very often a business person needs to evaluate whether this is a defect and how critical it is. Next, the team has to add the ticket to the sprint on a planning session.
Finally, you can pick the ticket from the backlog and start working on it. Probably, some time had passed since you have implemented this feature so you need to get familiar with the code and remember how it works. After a while, you are done and you create a pull request. Then, other team member does a code review and merges your changes. Last but not least, a tester needs to validate if the issue has been resolved. When everything is ok, he finally closes the ticket.
The situation is even worse when an end-user encounters a bug in your software. The process of fixing it is in general the same, but the company’s image is priceless. The last thing you want is to make your clients think that developers lost control of their software!
Having this in mind, we, as programmers, should make every effort to detect potential defects as soon as possible. Any tool or technique, which makes it easier is invaluable for us!
The best is yet to come
This concludes the first part of this article. But we have just scratched the surface! We have discussed what Sealed Classes are and what is so special about them. Also, we have built a fully functional Flutter application using the BLOC pattern. We have discovered some of the most common pitfalls of state management and how we can improve our code to make it less error-prone by using Sealed Classes.
In the next part, we will explore how to implement Sealed Classes in Flutter. Spoiler alert: for the time being, Dart doesn’t support this concept on the language level. But don’t forget, that Flutter has one of the greatest communities! As a result, developers have created several packages that provide Sealed Classes functionality.
Thus, we will take a closer look at Sealed Unions, Sum Types, Sealed Class, Super Enum, and Freezed packages and discover their pros and cons. As you can see there’s a lot for us to cover so stay tuned!
As a reminder, all the source code is available on GitHub.