Post

DoSo: Simple and Elegant Error and State Handling for Flutter

Why Grupo SBF's mobile team built DoSo to replace dartz and simplify error and state handling in Flutter.

Leia em português

Originally published on Medium on May 21, 2025.

This article is an English translation of the original article written in Portuguese, you can find it here.

If you work with Flutter on a daily basis, you’ve probably come across the dartz, bloc, and freezed libraries at some point. Even if you haven’t worked with them directly, it’s likely you’ve been impacted by a project that uses them, or at least read an article proposing an architecture (usually Clean Architecture) that includes them as part of the solution. That’s no surprise, as they are quite popular libraries: together, they’ve surpassed 4 million downloads and are widely used in Flutter projects, especially larger ones. They complement each other and are often used together to form a robust and elegant architecture.

At SBF, it’s no different. Although we don’t use freezed, since we try to minimize the use of code generation via build_runner (a topic for another article), the other libraries are part of our daily development.

We use bloc as the state manager for our screens, always opting for the Cubit pattern due to its simplicity, a principle that constantly guides us. dartz, on the other hand, is mainly used for error handling in API calls, following the concept of functional programming through the Either type. All of this is integrated into an architecture based on SOLID principles and Clean Architecture, always tailored to our needs and favoring simplicity, as mentioned before.

This architecture has taken us far: today, it’s the standard adopted in nearly 50 feature and engineering modules that make up what we call the Mobile Platform. These modules are used, in whole or in part, across more than four different applications.

With all this accumulated experience, a few red flags began to emerge. The dartz library hasn’t received updates for over three years (basically since we started using it), never reached a stable version (1.0.0), and completely lacks documentation. This situation has prevented us from adopting even more functional programming concepts. Additionally, the tight coupling with third-party libraries increases our exposure to vulnerabilities and limits our ability to evolve certain parts of the system.

We’ve always aimed to abstract libraries, SDKs, and APIs in the Mobile Platform to reduce coupling and direct dependencies. However, this isn’t always feasible. Some libraries are naturally hard to abstract, and the required effort may make the final solution more complex than the benefit of simply accepting them as part of the system.

bloc, freezed, and any library based on build_runner are good examples of this. On the other hand, dartz is relatively simple to abstract, as long as you don’t commit to using all of its features, which is our case. Still, this decision wasn’t made in the past. Over time, we’ve adopted a few simple measures to minimize the dependency, like using typedefs to create an alias for the Either return type, and applying lints like unused_local_variable to avoid explicitly declaring types in local scopes, thus reducing the need to import dartz in every file. Here’s an example:

Figure 1. Examples to reduce coupling. Figure 1. Examples to reduce coupling.

Even so, mentioning and importing dartz is still unavoidable, especially during test writing. This raises the question: wouldn’t it be better to remove this dependency before we face a vulnerability or serious incompatibility with this abandoned library?

A modern alternative is fpdart, which also provides functional programming tools and comes with complete documentation. Despite its robust proposal, the project hasn’t seen updates in about six months, as it was waiting for the release of static metaprogramming, the well-known macros in Dart. However, this feature was recently officially shelved by the Dart team, which may impact the library’s future.

Given this context, it’s worth reflecting: does it make sense to replace dartz with fpdart, adding new abstractions just to avoid direct coupling? Wouldn’t that lead us to repeat the same mistakes of the past? And most importantly: do we even need a full-featured functional library if, in practice, we only use a small fraction of its capabilities?

These questions led to the creation of DoSo: a library built by the mobile team at Grupo SBF to handle error management in a simple and elegant way. DoSo consolidates our 3+ years of experience with the Mobile Platform, enhancing and simplifying solutions to deliver higher quality and productivity to developers, reducing boilerplate code with an intuitive syntax and no need for build_runner or generated code.

The library brings together the most used features from dartz, incorporates ideas from fpdart and even freezed. DoSo is not just a tool for error handling, but also for state handling, reducing up to one-third of the lines required to achieve the same outcome as before.

Got questions? Let’s look at some examples!

Here’s a typical Remote Data Source in the Mobile Platform. It makes an API request and returns a response model. In this example, we’re dealing with a favorites page. Both success and failure are wrapped in Right/Left, and the return type is Result, our alias for Either:

Figure 2. API request example using dartz. Figure 2. API request example using dartz.

Here’s the same example using DoSo. Do represents an action. So is an alias of Do and represents the result. In our example, tryCatch executes the operation and automatically wraps the result with Do.success or Do.failure. The onCatch is optional and can be used to customize the returned exception. As you’ll see, the number of lines is almost cut in half:

Figure 3. Refactored example using DoSo. Figure 3. Refactored example using DoSo.

Alright, Lukita, I got the part about Do (Either), Do.success (Right), Do.failure (Left) with Do.tryCatch handling errors automatically… but what about state management you said replaces freezed?

Well, to avoid confusion: DoSo does not replace freezed, as they serve different purposes. However, DoSo offers a simpler alternative for declaring states in Cubits, without generated code. That’s not its primary role, so it’s not meant to be as flexible as freezed. Think of DoSo as a way to standardize and simplify the declaration of common states: Initial, Loading, Success, and Failure.

If we already created Do.success and Do.failure, why not also Do.initial and Do.loading? With this, we have fold to handle success and failure (like Right/Left in dartz), and when to deal with standard states.

Let’s see it in action. Here’s a Cubit and screen from a simple feature that waits for a request to return in our platform using dartz:

Figure 4. Simple Cubit with standard states using dartz. Figure 4. Simple Cubit with standard states using dartz.

Figure 5. Screen listening to the Cubit’s states. Figure 5. Screen listening to the Cubit’s states.

Now using DoSo: our Cubit changes little, but the state is now declared in a single line instead of several:

Figure 6. Cubit and state refactored with DoSo. Figure 6. Cubit and state refactored with DoSo.

On the screen, we use the when method to handle states clearly and concisely:

Figure 7. Refactored screen using DoSo. Figure 7. Refactored screen using DoSo.

It’s worth noting that DoSo was created to cover simple and common use cases for screens with basic states, such as loading, success, failure, or initial state. It does not aim to replace more robust approaches for complex state handling scenarios, such as screens with multiple fields, nested states, or many variations. In those cases, more complete solutions specific to your context may be better suited.

That said, nothing stops you from using DoSo in more complex screens, as long as you take a strategic approach. A recommended practice is to break large cubits/blocs that handle multiple responsibilities into smaller, specialized cubits, each managing a specific part of the screen’s state.

The image below illustrates a practical example: a screen displaying a list of offers from an API and, at the same time, showing the cart total at the bottom. This total needs to be updated dynamically as the user interacts with each product’s “+” or “-” buttons. By splitting this screen into cubits with distinct responsibilities (one for loading offers and another for controlling the cart), it becomes possible to use DoSo simply, even in more elaborate interfaces.

Figure 8. Hypothetical representation of a simple shopping cart screen. Figure 8. Hypothetical representation of a simple shopping cart screen.

Let’s compare the two approaches for handling this same fictional screen. Starting with the traditional solution using a single Cubit that manages all the screen’s states:

Figure 9. Traditional cubit example Figure 9. Traditional cubit example

And with DoSo, splitting into smaller, simpler Cubits with a single responsibility:

DoSo: Simple and Elegant Error and State Handling for Flutter

Figure 10. Refactored cubit with DoSo Figure 10. Refactored cubit with DoSo

As we’ve seen, using DoSo in combination with an architecture based on smaller, specialized Cubits results in a clearer and leaner approach to state modeling, even on screens with seemingly complex logic. This strategy, along with the conciseness and readability benefits that DoSo offers, reinforces its role as a powerful tool to simplify the (much-discussed) state handling and error handling in Flutter.

Beyond the features presented, DoSo also includes popular functionalities like maybeWhen, getOrElse, map, and flatMap. We’re currently refactoring modules that used dartz to adopt DoSo, which is already stable and in production in our environment.

If you like the idea behind DoSo and want to see more technical details, usage examples, and how to apply it to your project, the repository is available on GitHub with complete documentation and practical examples to ease adoption.

Disclaimer: The name DoSo is just a pun on the word “どうぞ” (dōzo) in Japanese which means: “please”, “go ahead”, or “here you go”, depending on the context.

This post is licensed under CC BY 4.0 by the author.