Post

DoSo: uma biblioteca Dart leve para tratar erros e estados de forma simples e elegante

Por que o time mobile do Grupo SBF criou o DoSo para substituir o dartz e simplificar o tratamento de erros e estados no Flutter.

Read in English

Publicado originalmente no sbf-tech, no Medium, em 12 de maio de 2025.

Se você trabalha com Flutter no dia a dia, provavelmente já se deparou com as bibliotecas dartz, bloc e freezed em algum momento. Mesmo que não tenha trabalhado diretamente com elas, é bem provável que tenha sido impactado por algum projeto que as utiliza, ou ao menos por algum artigo propondo uma arquitetura (geralmente Clean Architecture) que as inclui como parte da solução. Isso não é surpresa, afinal, são bibliotecas bastante populares: juntas, somam mais de 4 milhões de downloads e são amplamente utilizadas em projetos Flutter, especialmente os de maior porte. Elas se complementam e geralmente são empregadas em conjunto para formar uma arquitetura robusta e elegante.

Na SBF, não é muito diferente. Embora não façamos o uso do freezed, já que procuramos minimizar o uso de código gerado via build_runner (motivo que podemos explorar em outro artigo), as demais bibliotecas estão presentes no nosso dia a dia.

Utilizamos o bloc como gerenciador de estado das nossas telas, sempre optando pelo padrão Cubit, devido à sua simplicidade, um princípio que nos guia constantemente. Já o dartz é usado principalmente para tratamento de erros em chamadas de API, seguindo o conceito da programação funcional por meio do tipo Either. Tudo isso está integrado a uma arquitetura baseada em princípios SOLID e Clean Architecture, sempre respeitando nossas necessidades e prezando pela simplicidade, como dito anteriormente.

Essa arquitetura nos levou longe: hoje, é o padrão adotado em quase 50 módulos de funcionalidades e engenharia que compõem o que chamamos de Plataforma Mobile. Esses módulos são usados, total ou parcialmente, em mais de quatro aplicativos diferentes.

Com toda essa experiência acumulada, alguns alertas começaram a surgir. A biblioteca dartz está há mais de 3 anos sem atualizações (basicamente, desde quando começamos a fazer o seu uso), nunca chegou a uma versão estável (1.0.0) e carece completamente de documentação. Esse cenário contribui para que não tenhamos adotado ainda mais conceitos da programação funcional. Além disso, o forte acoplamento com bibliotecas de terceiros aumenta nossa exposição a vulnerabilidades e limita a evolução em certos aspectos.

Sempre buscamos abstrair bibliotecas, SDKs e APIs na Plataforma Mobile para reduzir acoplamento e dependências diretas. No entanto, nem sempre isso é viável. Algumas bibliotecas são naturalmente difíceis de abstrair, e o esforço necessário pode tornar a solução final mais complexa do que o benefício de simplesmente aceitá-las como parte do sistema.

O bloc, o freezed e qualquer biblioteca baseada em build_runner são bons exemplos disso. Por outro lado, o dartz é relativamente simples de abstrair, desde que você não se comprometa a usar todos os seus recursos, como é o nosso caso. Ainda assim, essa decisão não foi tomada no passado. Com o tempo, até adotamos algumas medidas simples para minimizar a dependência, como o uso de typedefs para criar um alias do retorno do Either, e o uso de lints como unused_local_variable para evitar a declaração explícita de tipos em escopos locais, reduzindo assim a necessidade de importar o dartz em todos os arquivos. Veja um exemplo abaixo:

Figura 1. Exemplos para diminuir o acoplamento. Figura 1. Exemplos para diminuir o acoplamento.

Apesar disso, a menção e importação do dartz ainda são inevitáveis, especialmente durante a escrita de testes. Surge então o questionamento: não seria melhor removermos essa dependência antes que surja uma vulnerabilidade ou incompatibilidade séria com essa biblioteca abandonada?

Uma alternativa moderna é o fpdart, que também oferece recursos de programação funcional e conta com uma documentação completa. Apesar de sua proposta robusta, o projeto está sem atualizações há cerca de seis meses, pois aguardava a chegada dos recursos de static metaprogramming, o famoso macros no Dart. No entanto, essa funcionalidade foi recentemente arquivada oficialmente pelo time do Dart, o que pode impactar os planos futuros da biblioteca.

Diante desse cenário, vale a reflexão: faz sentido substituir o dartz pelo fpdart, adicionando novas abstrações apenas para evitar o acoplamento direto? Será que isso não nos levaria a repetir os mesmos equívocos do passado? E, principalmente, será que precisamos mesmo de uma biblioteca funcional completa se, na prática, utilizamos apenas uma fração mínima de suas funcionalidades?

A partir desses questionamentos, eis que surgiu o DoSo: a biblioteca criada pelo time mobile do Grupo SBF para lidar com tratamento de erros de forma simples e elegante. O DoSo consolida nossos 3 anos de experiência com a Plataforma Mobile, aprimorando e simplificando soluções para entregar mais qualidade e produtividade aos desenvolvedores, reduzindo código repetitivo (boilerplate) com uma sintaxe intuitiva e sem necessidade de build_runner ou código gerado.

A biblioteca reúne os recursos mais utilizados por nós do dartz, incorpora ideias do fpdart e até do freezed. O DoSo não é apenas uma ferramenta de tratamento de erros, mas também de tratamento de estados, reduzindo em até 1/3 a quantidade de linhas necessárias para cumprir o mesmo papel da implementação anterior.

Dúvidas? Vamos a alguns exemplos!

Este é um típico Remote Data Source na Plataforma Mobile. Ele realiza uma requisição para uma API e retorna um modelo de resposta. No exemplo, estamos lidando com uma página de favoritos. Tanto o sucesso quanto a falha são encapsulados em Right/Left, e o retorno é o Result, nosso alias para o tipo Either:

Figura 2. Exemplo de requisição com dartz. Figura 2. Exemplo de requisição com dartz.

Aqui está o mesmo exemplo usando o DoSo. O Do representa uma ação. O So é um alias do Do e representa o retorno. No nosso exemplo, tryCatch executa a operação e encapsula automaticamente o resultado com Do.success ou Do.failure. O onCatch é opcional, usado para personalizar a exceção retornada. A quantidade de linhas, como você verá, é praticamente reduzida pela metade:

Figura 3. Exemplo refatorado usando o DoSo. Figura 3. Exemplo refatorado usando o DoSo.

Beleza, Lukita, entendi a parte do Do (Either), Do.success (Right), Do.failure (Left) com o Do.tryCatch fazendo o tratamento automático de erros… mas e o gerenciamento de estado que você disse que substitui o freezed?

Bom, para evitar confusão: o DoSo não substitui o freezed, pois eles têm propósitos diferentes. Contudo, o DoSo oferece uma alternativa mais simples para a declaração de estados em Cubits, sem código gerado. Essa não é sua função principal, portanto não visa ser tão flexível quanto o freezed. Pense no DoSo como uma forma de padronizar e simplificar a declaração dos estados mais comuns: Initial, Loading, Success e Failure.

Se já criamos Do.success e Do.failure, por que não também Do.initial e Do.loading? Com isso, temos o fold para tratar sucessos e falhas (como o Right/Left do dartz) e o when para lidar com os estados padrão.

Vamos ver na prática. Aqui está um Cubit e uma tela de uma funcionalidade simples que espera apenas o retorno de uma requisição em nossa plataforma usando dartz:

Figura 4. Cubit simples com estados padrão usando dartz. Figura 4. Cubit simples com estados padrão usando dartz.

Figura 5. Tela escutando os estados de um Cubit. Figura 5. Tela escutando os estados de um Cubit.

E agora, usando o DoSo: nosso Cubit muda pouco, mas o estado passa a ser declarado em uma única linha, enquanto antes exigia várias:

Figura 6. Cubit e estado refatorado com DoSo. Figura 6. Cubit e estado refatorado com DoSo.

Na tela, usamos o método when para manipular os estados de forma clara e concisa.

Figura 7. Tela refatorada com o DoSo. Figura 7. Tela refatorada com o DoSo.

Vale lembrar que o DoSo foi criado para cobrir casos simples e comuns de telas que envolvem estados básicos, como carregamento, sucesso, falha ou estado inicial. Ele não tem a pretensão de substituir abordagens mais robustas em cenários onde o gerenciamento de estado é mais complexo, como em telas com múltiplos campos, estados aninhados ou muitas variações. Nesses casos, pode ser mais adequado recorrer a soluções mais completas e específicas ao seu contexto.

Ainda assim, nada impede o uso do DoSo em telas mais complexas, desde que com uma abordagem estratégica. Uma prática recomendada é quebrar cubits/blocs grandes, que concentram múltiplas responsabilidades, em cubits menores e especializados, cada um cuidando de uma parte específica do estado da tela.

A imagem abaixo ilustra um exemplo prático: uma tela que exibe uma lista de ofertas vinda de uma API e, ao mesmo tempo, mostra o total do carrinho na parte inferior. Esse total precisa ser atualizado dinamicamente à medida que o usuário interage com os botões “+” ou “-” de cada produto. Ao dividir essa tela em cubits com responsabilidades distintas (uma para o carregamento das ofertas e outra para o controle do carrinho), torna-se possível aproveitar o DoSo com simplicidade mesmo em interfaces mais elaboradas.

Figura 8. Representação hipotética de uma tela simples para um carrinho de compras. Figura 8. Representação hipotética de uma tela simples para um carrinho de compras.

Vamos comparar as duas abordagens para lidar com essa mesma tela fictícia. Começando pela solução tradicional utilizando um único cubit que gerencia todos os estados da tela:

DoSo

E com o DoSo, dividindo em cubits menores, mais simples e com responsabilidade única:

DoSo

DoSo

Como vimos, o uso do DoSo em conjunto com uma arquitetura baseada em cubits menores e especializados proporciona uma abordagem mais clara e enxuta na modelagem de estados, mesmo em telas com lógica aparentemente mais complexa. Essa estratégia, aliada aos benefícios de concisão e legibilidade que o DoSo oferece, reforça seu papel como uma ferramenta poderosa para simplificar o (bastante discutido) gerenciamento de estados e tratamento de erros no Flutter.

Além dos recursos apresentados, o DoSo também oferece funcionalidades populares como getOrElse, map e flatMap. Atualmente, estamos refatorando os módulos que usavam dartz para adotarem o DoSo, que já se encontra estável e em produção no nosso contexto.

Se você curtiu a proposta do DoSo e quer ver mais detalhes técnicos, exemplos de uso e como aplicá-lo no seu projeto, o repositório está disponível no GitHub com documentação completa e exemplos práticos para facilitar sua adoção.

Esta postagem está licenciada sob CC BY 4.0 pelo autor.