Flutter MobX - Gestão de Estado de forma simples
Flutter MobX parece um casamento perfeito e, se você procura uma solução de gestão de estado para os seus apps, talvez este seja o casal certo. Neste artigo, você vai sair de um contador comum até uma tela que se atualiza sozinha, sem chamar o setState.
Antes de falar de MobX, é importante entender o que é gestão de estado e quais modelos temos no Flutter. Assim, fica mais fácil perceber por que o MobX se destaca.
O que é o MobX
Antes de mais nada, o MobX não é exclusivo do Flutter. Ele também existe para outras plataformas, como a web, onde nasceu no ecossistema JavaScript.
O MobX é uma biblioteca de gestão de estado. Isso significa que podemos usá-lo para estados locais, de uma página, ou globais, combinando com o Provider, por exemplo.
A biblioteca se baseia em observáveis, ações e reações, um conceito que vem da programação reativa. Segundo a documentação oficial do MobX para Dart, tudo gira em torno de observables, actions e reactions.
Na prática, uma ação altera o estado, e essa mudança dispara uma reação que atualiza quem está observando aquele valor.
Flutter MobX e gestão de estado
No Flutter, a tela não é renderizada o tempo todo. Por isso, precisamos chamar o setState para avisar que algo mudou, e isso se complica à medida que surgem atualizações em widgets distantes ou em outras árvores da aplicação.
O Flutter MobX resolve isso criando um observador ao redor do widget que queremos manter atualizado. Então, nesse widget, referenciamos uma propriedade marcada como @observable e, a cada alteração, o resultado aparece na tela.
A única regra aqui é que você deve passar por uma ação, marcada com @action, para alterar um observável. Na verdade, até é possível alterar um observável sem uma ação, mas a ideia é concentrar as regras de negócio nas ações e manter tudo organizado.
Essa ação produz uma reação que atualiza o observável na tela, como descrevi no começo desta seção.
MobX vs Streams
Neste momento, você deve estar se perguntando se as Streams não fazem exatamente o mesmo.
Sim, as Streams também conseguem atualizar uma propriedade sem o uso do setState. Porém, elas exigem muito mais código e você precisa gerenciá-las bem, pois Streams esquecidas abertas viram um caos.
Claro que toda essa mágica tem um custo, e o MobX também precisa de bastante código para funcionar. A diferença é que ele gera boa parte desse código automaticamente para nós.
Por exemplo, em vez de criar uma Stream de entrada, de saída ou ambas para um observável, basta decorar a propriedade com @observable. Dessa forma, o código adicional é gerado por baixo dos panos.
Com isso, teremos um arquivo .g.dart para cada arquivo que usar MobX. Esses arquivos nunca devem ser alterados à mão, porque toda alteração se perde na próxima geração.
Instalação do Flutter MobX
Para usar o Flutter MobX, vamos separar a instalação em duas partes. Primeiro, as dependências da aplicação, que de fato serão empacotadas com o app.
Além delas, teremos as dependências de desenvolvimento, usadas apenas na nossa máquina para gerar o código adicional que comentei.
Criando a aplicação
Se você ainda não configurou o seu ambiente para trabalhar com Flutter, veja o artigo sobre como começar com Flutter.
Depois, abra o seu emulador ou simulador favorito e uma instância do Visual Studio Code. Em seguida, pressione CTRL (CMD) + SHIFT + P e selecione Flutter: New Project.
No meu caso, defini o nome do projeto como flumobx. Se quiser usar o mesmo nome para total compatibilidade, fique à vontade, pois os fontes estão no final deste artigo.
Adicionando os pacotes
A primeira coisa a fazer é abrir o arquivo pubspec.yaml e adicionar as seguintes dependências.
dependencies:
flutter:
sdk: flutter
mobx: ^1.0.0
flutter_mobx: ^1.0.0
A dependência mobx traz o código base do MobX para a aplicação. Já a flutter_mobx traz a integração do MobX com o Flutter, e precisamos das duas.
Adicionando as dependências de desenvolvimento
Como comentamos, o MobX faz mágica gerando código para nós. Porém, ele precisa de dois pacotes para executar essa geração.
O primeiro é o mobx_codegen, que de fato olha para os observáveis e gera o código necessário para manter a tela atualizada.
O segundo é o build_runner, que interage com o mobx_codegen e possibilita a geração do código. Como resultado, temos as seguintes dependências de desenvolvimento.
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^1.7.2
mobx_codegen: ^1.0.0
Ao salvar o arquivo, o Flutter tenta restaurar os pacotes. Se tudo der certo, você verá a mensagem exit code 0 na aba Output do Visual Studio Code.
Se preferir, execute o comando flutter packages get manualmente.
Adicionando um Store
Quando falamos em gestão de estado, precisamos de um local para armazená-lo. No MobX, assim como no Redux, chamamos esse local de Store.
Podemos ter vários Stores, inclusive um se comunicando com outro, mas esse é um assunto mais avançado. Aqui, vamos focar em um único Store que servirá de base para a tela.
Primeiro, vamos retomar a ideia de separar as regras de negócio, tirando-as dos widgets e colocando-as em ações.
Então, antes de pensar em MobX, observe a tela inicial do app padrão gerado pelo Flutter. Nela, temos um número exibido na tela e um botão para incrementá-lo.
Nessa situação, temos claramente uma variável numérica e uma função que incrementa esse valor em um sempre que o botão é pressionado.
Pensando assim, vamos criar a classe lib/counter.dart com o mínimo de código possível.
class Counter {
int value = 0;
increment() {
value++;
}
}
Utilizando o contador na tela
Com a primeira versão do contador criada, que ainda não é um Store, vamos refatorar a tela para usá-lo. O primeiro passo é importá-lo no topo do arquivo lib/main.dart e criar uma instância da classe Counter.
import 'package:flumobx/counter.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
final counter = Counter();
Dessa forma, não precisamos mais da variável _counter, declarada na classe _MyHomePageState. Então, vamos excluí-la e usar o nosso contador.
Toda vez que clicamos no botão, a função _incrementCounter é chamada. Por isso, vamos alterá-la para chamar o contador em vez de incrementar manualmente.
void _incrementCounter() {
setState(() {
counter.increment(); // Antes era _counter++;
});
}
Note que incrementar um valor, embora seja algo básico, não deixa de ser uma regra de negócio. Agora, essa regra está no contador, e não mais na tela.
Ainda falta uma alteração na exibição do valor, que é um Text quase no fim do código. Agora, ele exibe o valor do contador em vez de _counter.
Text(
'${counter.value}', // Antes era $_counter
style: Theme.of(context).textTheme.display1,
),
Pronto, ao reiniciar a aplicação, tudo deve funcionar como antes, mas com uma separação melhor de código. Até aqui, o trecho principal do lib/main.dart fica assim, já sem os comentários do template.
class _MyHomePageState extends State<MyHomePage> {
void _incrementCounter() {
setState(() {
counter.increment();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${counter.value}',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Aplicando o Flutter MobX no contador
Até agora, apenas separamos o código e não usamos nada do MobX. Tanto que a tela ainda depende do setState para reagir às mudanças.
Então, vamos refatorar o contador para usar os recursos do Flutter MobX e aproveitar tudo o que comentamos até aqui.
Transformando o contador em Store
A primeira coisa a fazer é transformar o contador em um Store, e para isso usamos um mixin. Os mixins são uma forma de reutilizar o código de uma classe em várias hierarquias de classes.
Parece difícil, mas faz parte do conceito de reaproveitamento que já conhecemos. Toda vez que usamos a palavra reservada with, estamos aplicando um mixin.
Dessa forma, para tornar a classe um Store, basta importar o pacote do MobX e adicionar o trecho with Store.
import 'package:mobx/mobx.dart';
class Counter with Store {
int value = 0;
increment() {
value++;
}
}
Agora, a classe Counter é um Store. Mas calma, ainda há mais coisas para discutir.
Observables no Flutter MobX
O próximo passo é fazer a mágica aparecer, ou seja, fazer a tela se atualizar sozinha, sem chamar o setState a cada mudança.
Para isso, precisamos de um observável, isto é, um valor que o widget fica sempre observando.
Esse modelo lembra o padrão publish/subscribe, em que vários widgets se inscrevem para receber uma notificação a cada alteração de uma variável.
No Flutter MobX, para tornar um valor observável, basta adicionar a anotação @observable sobre a variável desejada.
import 'package:mobx/mobx.dart';
class Counter with Store {
@observable
int value = 0;
increment() {
value++;
}
}
Pronto, já temos no Store uma variável observável, mas o trabalho não terminou.
Actions no Flutter MobX
Lembra da regra de que os observáveis só devem ser alterados por meio de ações?
O contador já tem esse comportamento, pois o método increment faz o incremento do valor. Portanto, basta decorar esse método com @action.
Afinal, as ações nada mais são do que métodos que contêm as regras e alteram os observáveis, ou seja, o estado.
import 'package:mobx/mobx.dart';
class Counter with Store {
@observable
int value = 0;
@action
increment() {
value++;
}
}
Pronto, temos um observável e uma ação.
Gerando o código do Flutter MobX
Se testarmos o código agora, nada muda, e ainda precisamos do setState para atualizar a tela. Isso acontece porque a mágica do MobX ainda não aconteceu.
Lembra que o MobX gera código para nós? Pois bem, agora precisamos pedir que ele gere o código que gerencia as ações e os observáveis decorados no Store.
Isso é feito com o comando abaixo.
flutter packages pub run build_runner build
Esse comando procura os arquivos que usam o mixin Store e gera um arquivo secundário para cada um, com a extensão .g.dart.
Sempre que alterarmos os Stores, precisamos executar o comando novamente, e novos arquivos .g.dart serão gerados. Por isso, de nada adianta editar esses arquivos à mão.
Além disso, existe o comando clean, que limpa todos os arquivos gerados. Caso enfrente algum problema, execute um clean e depois um build para garantir que tudo foi gerado corretamente.
flutter packages pub run build_runner clean
A documentação do MobX também cita o modo watch, que gera o código automaticamente a cada alteração salva. Nesse caso, a opção --delete-conflicting-outputs remove arquivos conflitantes antes de gerar novamente.
flutter pub run build_runner watch --delete-conflicting-outputs
Porém, não adianta executar o build ainda. Precisamos de uma última modificação no Store para finalizá-lo.
Unificando o Store com o código gerado
Até aqui, vimos como externalizar as regras, trabalhar com Store e gerar o código do MobX. Agora, precisamos unir o código gerado ao nosso, formando um único Store para o app consumir.
Primeiro, importamos o código gerado. Não se preocupe se ele ainda não existe, afinal, já sabemos que ele vai existir e qual será o seu nome.
Então, logo após a importação do MobX no lib/counter.dart, declaramos o arquivo complementar com a diretiva part.
part 'counter.g.dart';
Note que o arquivo gerado sempre segue o padrão nome-do-arquivo.g.dart.
Todo arquivo gerado pelo MobX contém código específico relacionado às anotações usadas no Store. Nosso desafio, então, é unir as duas partes em uma só classe.
Como vimos, podemos fazer essa junção com um mixin, assim como fizemos com o Store. A classe gerada segue um padrão e coloca _$ na frente do nome da nossa classe, então Counter gera _$Counter.
Sabendo disso, podemos criar uma nova classe no lib/counter.dart que une as duas.
class MyCounter = Counter with _$Counter;
Este ainda não é o código final, mas note que criamos a classe MyCounter, que é a junção do nosso Counter com o código gerado.
Limitando o acesso com classe abstrata
Essa implementação ainda não está completa. Nesse formato, nada impede alguém de criar uma instância de Counter em vez de usar o MyCounter.
Na verdade, quem consumir esse código ficará confuso, pois há duas classes de contador no arquivo. Qual é a correta?
É aqui que entra um conceito muito útil da orientação a objetos: as classes abstratas.
As classes abstratas definem comportamentos padrão, mas não podem ser instanciadas. Ou seja, não conseguimos criar um objeto diretamente a partir delas.
Além disso, o nome MyCounter não ficou muito bom. Por isso, seguindo a convenção sugerida pela documentação do MobX, usamos um _ no nome da classe abstrata.
Assim, o nome Counter fica livre para a classe final usada pelo app. O código final do contador fica assim.
import 'package:mobx/mobx.dart';
part 'counter.g.dart';
class Counter = _Counter with _$Counter;
abstract class _Counter with Store {
@observable
int value = 0;
@action
increment() {
value++;
}
}
Para finalizar, executamos novamente o comando de geração.
flutter packages pub run build_runner build
Se tudo deu certo, agora você tem um arquivo lib/counter.g.dart e o código compila corretamente.
Observer no Flutter MobX
Estamos quase lá, faltam apenas algumas modificações na tela. A primeira delas é importar o pacote do Flutter MobX no lib/main.dart.
import 'package:flutter_mobx/flutter_mobx.dart';
A segunda modificação é excluir a função _incrementCounter, já que podemos chamar diretamente o método increment do Counter no botão.
Para isso, precisamos de uma instância do Counter, definida logo abaixo do método main.
final counter = Counter();
Essa instância está marcada como final porque nunca será alterada depois de criada.
Seguindo para o botão, quase no fim do arquivo, alteramos a propriedade onPressed para chamar o método increment do contador.
onPressed: counter.increment,
Note que, ao passar o método increment, não abrimos parênteses. Caso contrário, o método seria chamado imediatamente, e não apenas quando o botão for pressionado.
Por fim, precisamos exibir o valor do contador, e para isso usamos um widget adicional chamado Observer.
Esse widget conecta a tela ao Store e reage às mudanças que acontecem lá. Se apenas colocarmos na tela um valor do Store, ele não vai reagir às alterações.
Então, envolvemos o Text que exibe o número de cliques com o Observer, neste formato.
Observer(
builder: (_) => Text(
'${counter.value}',
style: Theme.of(context).textTheme.display1,
),
),
O Observer não tem um child, e sim um builder, que é uma função. Como não usamos o parâmetro dessa função, simplesmente o nomeamos como _.
Segundo a documentação, o simples ato de ler counter.value dentro do Observer já faz com que ele passe a rastrear o valor e redesenhe a tela a cada mudança.
O código final do arquivo lib/main.dart fica assim.
import 'package:flumobx/counter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
void main() => runApp(MyApp());
final counter = Counter();
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Observer(
builder: (_) => Text(
'${counter.value}',
style: Theme.of(context).textTheme.display1,
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: counter.increment,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Repare que o setState desapareceu completamente da página. Agora, quem decide quando redesenhar o número é o próprio Observer.
Sobre as versões usadas
Este exemplo usa a linha 1.x dos pacotes mobx, flutter_mobx e mobx_codegen, além do estilo de código da época. Por isso, alguns detalhes mudaram em versões mais recentes do Flutter.
Por exemplo, o estilo display1 do TextTheme foi renomeado em versões posteriores. Da mesma forma, o comando flutter packages pub run deu lugar a formas mais curtas, como o flutter pub run citado na documentação do MobX.
Ainda assim, os conceitos continuam os mesmos. Observables, actions, reações e o widget Observer seguem como a base do Flutter MobX.
Conclusão
Parabéns, você criou o seu primeiro app com Flutter MobX e, espero, entendeu os conceitos básicos da biblioteca.
Em resumo, separamos a regra de negócio em um Store, marcamos o estado com @observable, alteramos esse estado com @action e deixamos o Observer cuidar da atualização da tela.
Ainda há muito a aprender sobre MobX, como reações, valores computados e a comunicação entre Stores. Para comparar com outra abordagem, veja também a implementação básica do padrão BLoC no Flutter.
Não deixe de conhecer a nossa carreira Flutter Mobile Developer, que tem vários cursos gratuitos e vai te dar uma boa base nos estudos.
Código fonte
O código fonte deste aplicativo está disponível no repositório flutter-mobx-getting-started no GitHub.
FAQ
O MobX é exclusivo do Flutter?
Não. O MobX surgiu no ecossistema JavaScript e tem uma implementação para Dart, usada no Flutter por meio dos pacotes mobx e flutter_mobx.
Por que preciso do build_runner para usar MobX?
Porque o mobx_codegen gera o código que torna observáveis e ações reativos. O build_runner é a ferramenta que executa essa geração e cria os arquivos .g.dart.
Posso editar os arquivos .g.dart?
Não. Esses arquivos são recriados a cada execução do build_runner, então qualquer alteração manual se perde.
O Observer substitui o setState?
Para os valores observáveis do Store, sim. O Observer rastreia os observáveis lidos no builder e redesenha apenas aquele trecho quando eles mudam.
Qual a diferença entre MobX e Streams?
As duas abordagens atualizam a interface sem setState, mas as Streams exigem mais código manual. O MobX gera esse código e reduz o risco de deixar Streams abertas.