Flutter: SetState, BLoC, MobX e State Management

O state management no Flutter ainda rende boas discussões, e sinceramente eu achei que esse assunto já estava esgotado. Como ainda existe margem para entendimentos diferentes sobre setState, BLoC e MobX, lá vamos nós de novo!

Este artigo fala de conceitos antes de falar de implementação. Mesmo assim, ao longo do texto você vai ver trechos curtos de código para fixar cada ideia.

One size doesn't fit all

Antes de continuar a leitura, quero que você tenha em mente algo muito importante. Talvez seja a lição mais importante que você pode aprender como desenvolvedor.

One size doesn't fit all.

Essa frase circula entre arquitetos de software e significa "um tamanho não serve para todos". Aqui no Brasil, costumamos dizer que arquitetura não tem receita de bolo ou que não existe bala de prata.

Mas por que isso importa? Porque tudo no mundo da tecnologia nasce com um propósito, e cada ferramenta tem seu valor e seu lugar certo de uso.

Não resolvemos tudo com setState, muito menos com BLoC e menos ainda com MobX. Afinal, cada app é um caso à parte, que exige estudo, tecnologia e implementação específicos.

Se você resolve tudo de uma única forma, está subestimando ou superestimando o projeto. Em alguns casos você até acerta, mas é impossível atender todos os cenários de forma viável.

Portanto, cabe ao desenvolvedor conhecer diferentes formas, implementações e arquiteturas. Só então, diante do problema, ele escolhe a solução.

O que é state management no Flutter

Estado é qualquer dado que pode mudar enquanto o app está rodando e que influencia o que aparece na tela. Por exemplo, o texto digitado em um campo, os itens de um carrinho ou o usuário autenticado.

O Flutter é um framework declarativo. Em outras palavras, a interface é uma função do estado: quando o estado muda, o framework reconstrói os widgets afetados.

Dessa forma, o state management no Flutter trata de duas perguntas. Onde o estado fica guardado e como os widgets descobrem que ele mudou?

A documentação oficial sobre estado efêmero e estado do app reforça a ideia deste artigo. Segundo ela, não existe uma regra universal para decidir qual técnica usar, e a escolha depende da complexidade do app.

setState, a base do state management no Flutter

Vamos começar pelo mais básico, e se você pulou essa parte, na minha opinião já começou errado. É importante entender o que é o setState e como usá-lo, porque ele é a base da mudança de estado no Flutter.

Afinal, como alguém aprende BLoC, MobX ou qualquer outra técnica sem saber o que acontece no setState? Também fica difícil entender por que existem alternativas a ele.

Um problema simples: validar CPF

Nosso primeiro problema é simples: temos um app que apenas valida CPF. Você tem um campo na tela, precisa recuperar o valor digitado e, em seguida, mostrar se o CPF é válido ou não.

Pergunta de entrevista: como você resolveria esse problema?

Sinceramente, se eu estivesse entrevistando você e, para um app simples desses, você apresentasse uma estrutura com BLoC ou MobX, você estaria reprovado.

Quando falamos em resolver problemas, falamos em fazer isso da melhor forma possível dentro do tempo disponível. Veja como o setState resolve tudo em poucas linhas.

import 'package:flutter/material.dart';

class DocumentPage extends StatefulWidget {
  @override
  _DocumentPageState createState() => _DocumentPageState();
}

class _DocumentPageState extends State<DocumentPage> {
  final controller = TextEditingController();
  String result = '';

  // Validação simplificada apenas para ilustrar o fluxo
  bool isValidDocument(String value) =>
      value.replaceAll(RegExp(r'[^0-9]'), '').length == 11;

  void validate() {
    // setState avisa o Flutter que o estado mudou
    setState(() {
      result = isValidDocument(controller.text) ? 'Valid' : 'Invalid';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          TextField(controller: controller),
          ElevatedButton(onPressed: validate, child: Text('Validate')),
          Text(result),
        ],
      ),
    );
  }
}

A função de validação acima apenas confere a quantidade de dígitos. Em um app real, você aplicaria o cálculo completo dos dígitos verificadores, mas o fluxo de estado continuaria o mesmo.

Simplicidade também é arquitetura

Se você adiciona complexidade ao projeto, também adiciona risco. Além disso, exige mais conhecimento do time, consome mais tempo e por aí vai.

Muitas vezes, o simples é o melhor que podemos ter.

Mas e a arquitetura? Olhe a simplicidade do app: ele não precisa de uma arquitetura elaborada.

Outros exemplos simples são os apps que desenvolvemos nos cursos de Flutter aqui do site, como Álcool ou Gasolina, Perfect Eggs e Todo List. São apps simples, que pedem soluções simples, ponto.

Diante desses fatos, você ainda acha que o setState é ruim para o state management no Flutter? Ainda acredita que pode ir direto para o MobX sem estudá-lo?

BLoC no state management do Flutter: padrão antes de implementação

No state management do Flutter, o BLoC é o mais incompreendido de todos, simplesmente porque muita gente associa o nome a uma única implementação. Isso é um erro.

Antes de falar de BLoC, precisamos falar sobre padrões. Um padrão é uma espécie de especificação, uma sugestão a seguir, e não tem a ver necessariamente com código.

Ou seja, você pode implementar um padrão de diversas formas, e está tudo bem. Por isso, um padrão nunca vai ser limitado ou ruim, porque ele depende do problema, e não o contrário.

O BLoC pode não atender a um cenário e atender perfeitamente a outro, assim como todos os padrões que conhecemos. Não usamos MVC em tudo, não usamos BLoC em tudo e não usamos MobX em tudo.

O que significa BLoC

O BLoC é um padrão relativamente novo, baseado em ideias antigas e até óbvias. O nome significa Business Logic Component, ou seja, componente de lógica de negócio.

A premissa dele é separar a lógica de negócio da interface do usuário. Isso faz todo sentido, já que no Flutter podemos ter interfaces distintas para Android e iOS, além de Web e Desktop.

Reescrever a mesma regra em cada tela seria suicídio, concorda? Então, se você tem um cálculo que roda no clique de um botão em mais de uma interface, coloque esse código em uma terceira classe e compartilhe com as telas.

Se você vai fazer isso com ChangeNotifier, com Streams ou com MobX, não importa para o state management no Flutter. Estamos falando de padrão e de conceito, não de implementação.

Inclusive, amanhã o Flutter pode mudar a forma de atualizar componentes, e as implementações de BLoC mudam junto. No entanto, o conceito nunca vai mudar.

Em resumo, sempre que você separa a regra da tela, você tem um BLoC. Se você trabalha com MVC e deixa a regra nos Controllers, separada das Views, você tem um BLoC; se usa MobX e deixa as regras nas Actions, também tem um BLoC.

Claro que, nesses casos, é mais simples chamar as peças de Controllers e Stores para facilitar o entendimento. Mesmo assim, se analisarmos bem, existe uma forma de BLoC dentro delas.

Por que saímos do BLoC puro

Se o BLoC resolve tudo isso, por que muita gente migrou dele? Quando o assunto é state management no Flutter, eu sempre aconselho meus alunos a aprender conceitos, porque eles são atemporais, enquanto as implementações mudam com o tempo.

Quando começamos com o BLoC puro, precisamos notificar as telas sobre as mudanças. Embora o setState faça isso, ele só reconstrói a árvore de widgets abaixo do State que o chamou.

Ou seja, se você precisa atualizar um componente que está na barra de navegação, o setState da tela não alcança esse widget.

Em seguida, olhamos para o ChangeNotifier. Na época, a própria documentação da classe dizia que ela era otimizada para poucos ouvintes, um ou dois, e por isso não a recomendávamos para apps com muitas telas observando o mesmo estado.

A alternativa que achamos conveniente foram as Streams, que são fluxos de dados. Com elas, os widgets ficam conectados ao fluxo e se atualizam automaticamente, sem chamar setState.

import 'dart:async';

// BLoC implementado com StreamController
class CartBloc {
  final _controller = StreamController<int>.broadcast();
  int _count = 0;

  Stream<int> get count => _controller.stream;

  void addItem() {
    _count++;
    _controller.sink.add(_count);
  }

  // Sem este dispose, a Stream continua aberta consumindo memória
  void dispose() => _controller.close();
}

Na tela, um StreamBuilder escuta a propriedade count e redesenha apenas o trecho necessário. Porém, as Streams escondem um perigo terrível: esquecer de fechá-las.

Se você se conecta a uma Stream quando o widget é montado e esquece o dispose, ela continua aberta e consumindo memória. Além disso, as Streams precisam de controladores, o que gera uma quantidade significativa de código repetitivo, algo que me incomodava bastante.

Nesse cenário do state management no Flutter, o MobX chegou detonando tudo. Embora ele gere arquivos adicionais por baixo dos panos, nosso código fica mais limpo.

Estado global versus estado local no state management do Flutter

Antes de falar de MobX, precisamos entender os dois cenários de state management no Flutter. O estado global está presente em todas as páginas, ou pelo menos disponível para elas, enquanto o estado local pertence a uma página ou componente.

O estado global aparece, por exemplo, quando autenticamos um usuário. Recebemos nome, e-mail, imagem e token, e deixamos essas informações disponíveis para todas as páginas.

Imagine que exibimos algum dado do usuário no cabeçalho ou no rodapé de várias telas. Essa informação precisa estar disponível, mas foi capturada lá no começo, na tela de login.

Estado global sem biblioteca

O que muita gente esquece é que ter estado global no state management do Flutter não significa usar MobX. Eu posso guardar o estado em variáveis globais.

Imagine um app em que o usuário é obrigado a se autenticar. Depois do login, ele vai para a Home e segue o jogo, e seus dados não mudam; se mudarem, ele precisa sair e entrar de novo.

Pergunta de entrevista: qual a forma mais simples de resolver esse cenário?

Se você respondeu MobX, Provider ou qualquer biblioteca de gestão de estado, errou. A maneira mais simples aqui é deixar o usuário em uma variável global, disponível para todo o app.

// Estado global simples: preenchido uma vez após o login
class User {
  final String name;
  final String email;
  final String token;

  User(this.name, this.email, this.token);
}

User currentUser;

Se você não precisa reagir a mudanças nem atualizar widgets automaticamente, não precisa de MobX ou Provider para ter estado global.

O mesmo vale quando o local onde você usa a variável global está ao alcance de um setState. Por exemplo, você adiciona itens ao carrinho, mas só precisa exibi-los na própria tela do carrinho.

Estado local e memória no state management do Flutter

O mesmo raciocínio vale para estados locais, como um formulário que envia dados. Nesses casos, eu não uso MobX nem estado global, porque nem faz sentido.

Tenho apenas um formulário na tela e, se precisar de alguma atualização, uso um setState e pronto. Simples e direto.

Entenda que, no state management do Flutter, estado é memória do dispositivo, e se você carrega tudo na memória, já sabe o resultado. Recentemente vi vários exemplos de CRUD usando MobX em todas as telas e, pior ainda, com Provider.

Imagine carregar todo o catálogo de produtos, ou boa parte dele, para a memória do dispositivo. Para mim, é suicídio!

O que é o MobX no state management do Flutter

Agora que já falamos de setState, BLoC e estados, vamos ao MobX. Ele funciona com uma tríade: observáveis, ações e reações.

O MobX é um pacote que implementa o state management no Flutter de forma simples. Na verdade, ele escreve boa parte do código para nós, por baixo dos panos, com geração de código.

Basicamente, definimos uma Store (o estado), que as Actions alteram e que os Observers acompanham nos widgets. Podemos tornar essa Store global com o Provider, por exemplo, e consumi-la em diversos widgets do app.

import 'package:mobx/mobx.dart';

part 'cart_store.g.dart';

class CartStore = _CartStore with _$CartStore;

// Store com estado observável e ações que o alteram
abstract class _CartStore with Store {
  @observable
  int count = 0;

  @action
  void addItem() {
    count++;
  }
}

Na tela, basta envolver o trecho que depende da Store com o widget Observer, do pacote flutter_mobx. Assim, só esse trecho é redesenhado quando count muda.

A grande vantagem do MobX sobre outras implementações está nas anotações. Elas deixam o código mais limpo, simples e fácil de ler, enquanto o mobx_codegen gera o restante.

Comparado à mesma implementação com BLoC e Streams, o MobX parece mágica. Para ver um passo a passo completo, confira nosso artigo sobre MobX no Flutter.

Devo usar MobX em todas as telas

Então bora usar MobX em tudo? Não!

Quem só conhece prego só dá martelada, já ouviu essa?

O MobX é incrível e tem ótimo suporte da comunidade e dos criadores do pacote. No entanto, no state management do Flutter, não podemos definir a ferramenta antes de conhecer o problema.

Como comentei em quase todo este artigo, dado o problema, fornecemos a solução. Dessa forma, o MobX não resolve tudo, assim como o BLoC também não.

Como escolher o state management no Flutter

Para facilitar a decisão, a tabela abaixo resume os cenários discutidos até aqui. Ela não é uma regra, mas ajuda a escolher o state management no Flutter de forma consciente.

Cenário Abordagem sugerida
Tela isolada, como formulário ou validação simples setState
Dado global que não muda depois de carregado Variável global
Regra compartilhada entre várias telas BLoC, com a implementação que fizer sentido
Estado global que precisa atualizar vários widgets MobX com Provider ou solução equivalente

Repare que a primeira pergunta é sempre a mesma: quem precisa saber dessa mudança? Se a resposta for apenas a própria tela, o setState provavelmente basta.

Para conhecer a implementação passo a passo do padrão, leia o artigo sobre a implementação básica do BLoC no Flutter.

O que eu aprendo então

Aprender a desenvolver apps é diferente de aprender Backend ou Web. Os apps têm formas e necessidades variadas, e não existe receita de bolo para o seu.

Amanhã você pode precisar desenvolver um eCommerce, que demanda uma gestão de estado global mais robusta, e optar pelo MobX. Em seguida, pode surgir um app que é apenas um catálogo, ou um app conceitual, que nem precisa de BLoC.

Com certeza, você precisa aprender diversas formas de fazer as coisas, para não ficar preso a uma só. Quanto mais soluções você tiver no seu leque, mais especialista você se torna, melhor resolve os problemas e mais rápido entrega.

Qual a minha recomendação

Depende do problema. Se você me chamar para desenvolver um app agora, muito antes de falar sobre tecnologia, vamos falar sobre negócio.

Talvez a solução nem seja em Flutter. Por isso, entender o negócio vem antes de escolher qualquer state management no Flutter.

Conclusão: conceitos acima do código

Primeiramente, se você leu este artigo até aqui, parabéns! O texto é longo, e acredito que poucos chegam ao fim.

Garanto que você aprendeu bastante, e quase sem ver código, correto? Esse é o ponto chave: falamos de conceitos, e cada um pode ter sua implementação.

Os conceitos são a parte mais importante do state management no Flutter, porque são atemporais. Linguagens, tecnologias e implementações surgem a toda hora, mas os fundamentos não mudam.

Então, se você tem uma base sólida, está apto a ir para qualquer lugar!

FAQ

O setState é uma má prática?

Não. O setState é a base da mudança de estado no Flutter e atende muito bem telas isoladas e estados locais.

BLoC é um pacote ou um padrão?

O BLoC é um padrão que separa a lógica de negócio da interface. Existem pacotes que o implementam, mas o conceito independe de qualquer biblioteca.

Quando o MobX vale a pena?

Quando vários widgets precisam reagir à mesma mudança de estado global, como em um carrinho de eCommerce exibido em diversas telas.

Posso misturar setState e MobX no mesmo app?

Sim. Inclusive é o recomendado: use setState nas telas simples e MobX apenas onde o estado compartilhado exige reatividade.

Por que preciso fechar uma Stream?

Porque uma Stream aberta continua ocupando memória mesmo depois que o widget sai da tela. Por isso, feche o controlador no dispose.

Glossário

Termo Significado
Estado Dado que muda durante a execução do app e influencia o que aparece na tela
setState Método do State que avisa o Flutter para reconstruir o widget
BLoC Business Logic Component, padrão que separa a regra de negócio da interface
Stream Fluxo assíncrono de dados que os widgets podem escutar
StreamController Classe que cria e controla uma Stream no Dart
ChangeNotifier Classe do Flutter que notifica ouvintes quando o estado muda
MobX Biblioteca de gestão de estado baseada em observáveis, ações e reações
Store Classe do MobX que concentra o estado observável e as ações
Provider Pacote que disponibiliza objetos para a árvore de widgets
Estado global Estado acessível por várias telas do app
Estado local Estado restrito a uma tela ou componente

Fontes