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.

Glossário

Termo Significado
MobX Biblioteca de gestão de estado baseada em observáveis, ações e reações.
Store Classe que guarda o estado e as ações que o modificam.
Observable Valor monitorado pelo MobX, que dispara reações quando muda.
Action Método que concentra a regra de negócio e altera os observáveis.
Observer Widget do flutter_mobx que redesenha a tela quando os observáveis lidos mudam.
Mixin Recurso do Dart para reutilizar código de uma classe em várias hierarquias, aplicado com with.
build_runner Ferramenta do Dart que executa geradores de código, como o mobx_codegen.
Classe abstrata Classe que define comportamentos, mas não pode ser instanciada diretamente.

Fontes