Pressione enter para ver os resultados ou esc para cancelar.

Gerência de Estados no Flutter

Neste post vamos introduzir o básico sobre Gerência de Estados no Flutter, além de descobrir o que é, porquê e como funciona. Não iremos nos aprofundar sobre o Widget ou afins da linguagem, talvez em uma próxima e se você tiver interesse pode deixar nos comentários ao final deste post. 😜

O que são “Estados”

Primeiro, o que é “Estado” afinal? Bom, tratando-se da palavra temos vários significados. O Brasil é composto por 26 estados, por exemplo, mas, no contexto de software o “Estado” é uma combinação do valor original mais as modificações ao longo do tempo, ou seja, “Estado” é uma informação em um determinado ponto do tempo, e essa informação pode ser um status, uma condição, transação, etc.

No nosso dia a dia podemos ver vários exemplos, como as luzes semáforo, falo sobre as luzes porque você pode ser chato e falar que o semáforo, na verdade, é uma máquina de estados finita, mas isso é assunto para outra hora. hehe 🙂

As luzes do semáforo tem dois estados, ligado e desligado, em conjunto elas formam o semáforo, que tem 3 estados sinalizados por cores, sendo: verde (sinaliza para os motoristas que o caminho está livre para ser seguido), amarelo (sinaliza que o sinal está prestes a fechar) e vermelho (sinaliza que a passagem está bloqueada), isso no ponto de vista do motorista, pois, os pedestres interpretam de outra maneira.

 

No Contexto de Software

Se você é do clube do JavaScript então você está acostumado com estados, seja mudando um valor na UI (User Interface) com a API nativa ou com o poder do ReactJS. No Flutter é bem parecido, vamos à algo mais visual, como, por exemplo, uma aplicação que gera um número aleatório de 1 até 1000:

Aplicação Flutter

Usando a API do ReactJS, você nota que usamos o hook useState e dele fazemos uma atribuição via desestruturação sendo uma variável (é um estado), number e uma função que chamamos de setNumber (define o “valor” do estado).

A lógica não importa tanto, preste atenção à variável number que é usada dentro da tag p no HTML, de acordo com o click, nós vamos mudando o valor dela mas também usamos ela para exibir, ou não, o número randômico, isso porque inicialmente ela é undefined, este é o estado inicial dela!

Flutter

A ideia é fazer o usuário clicar no botão para obter um novo número, e este é o resultado:

Flutter

Vamos dar uma olhada em como fazemos isso no Flutter! Vou considerar que você já sabe o básico de Dart e o básico sobre Widgets.

class _MyHomePageState extends State<MyHomePage> {
 int? number;
 int generateRandomNumber() {
   return (new Random()).nextInt(1000) + 1;
 }
 void handleClickEvent() {
   setState(() => {number = generateRandomNumber()});
 }
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: Text(widget.title),
     ),
     body: Container(
       child: Center(
         child: Column(
           mainAxisAlignment: MainAxisAlignment.center,
           children: [
             SizedBox(
               height: 40.0,
               child: Text(
                 number == null
                     ? 'Click in the button bellow to generate random number'
                     : "Random number: $number",
               ),
             ),
             ElevatedButton(
               onPressed: handleClickEvent,
               child: Text('Generate a number!'),
             ),
           ],
         ),
       ),
     ),
   );
 }
}

Hehe, mudamos para o Dart então as coisas são um pouco diferentes, mas parecidas, fizemos a mesma coisa e olha, temos um setState (setNumber) também!
Temos praticamente o mesmo resultado visualmente:Flutter Gif
Depois do click:
Flutter
Mas, pô, praticamente? Sim! Diferente do React, o setState ou setNumber, redesenha toda a UI (para ser mais justo, o método build é executado novamente)! Se você olhar o código fonte para ver como o setState funciona, verá que ele marca como “needsBuild” um dos 60 frames e assim o framework sabe que é necessário um rebuild.

Você consegue usar o setState para construir uma aplicação inteira, com um certo nível de acoplamento você pode nem precisar usar!

Porém, a coisa vai ficar complicada dependendo do peso da sua aplicação, das telas, e dos contextos que precisarão de uma informação que está em outra seção, dos componentes e até do backend

Vamos ver na pŕatica o setState redesenhando a UI. A única coisa que eu vou mudar no código é o título da appBar e por enquanto, vou tirar o setState de cena:

Aplicação Flutter

Estamos concatenando o título + o resultado da função, e mesmo clicando no botão, nada acontecerá!

Esse comportamento faz sentido visto que a tela já foi desenhada, e aquele número vai ficar estático até que algo redesenhe a tela de novo! Então, colocando o setState de volta, perceberemos que o valor não é mais estático, ele muda a cada click! Mesmo que eu não esteja usando o number (estado) lá!

Isso acontece, pois, mesmo usando um widget stateless, um widget que não precisamos manter o estado dele, o mesmo é re-renderizado, pois, o build acontece novamente!

Então como podemos fazer para que o comportamento seja o mesmo comportamento do React?

Para atualizar apenas o local em que usamos o estado (listeners), precisamos de um Gerenciador de Estado, temos vários e você certamente encontrará suporte da comunidade em qualquer um deles: GetX, BloC, MobX entre outros…

Há uma grande briga na comunidade sobre qual é melhor ou coisas do tipo, cada um tem suas nuances e para os próximos exemplos irei usar o GetX.

Existem algumas formas de fazer esse app genérico de gerar números aleatórios, vamos usar um controller no exemplo para separar a view da lógica. Você pode conferir a sintaxe e como usar o Get na documentação do componente [https://pub.dev/documentation/get/latest/]. Nossa lógica no Get ficará assim:

class ControllerExample extends GetxController {
 var number = 0.obs;
 int generateRandomNumber() {
   return (new Random()).nextInt(1000) + 1;
 }
 void handleClickEvent() {
   number.value = this.generateRandomNumber();
 }
}

Não seria tão diferente usando o ChangeNotifier ou outros packages. No caso do Get, para indicarmos que uma variável é um estado, colocamos a notação .obs, de observable, no valor inicial da variável. Por quê?
Nesse exemplo estamos usando um dos tipos de gerenciamento de estados do Get, o Gerenciamento Reativo! O Get nos provê dois tipos: o Gerenciamento de Estados Simples e o Gerenciamento Reativo.

Simples x Reativo

Explicando brevemente a diferença entre os dois, quando não precisamos passar o estado adiante (para outras telas) usamos o Gerenciamento Simples, o nosso código ficaria assim:

class Example extends StatelessWidget {
 var number = 0.obs;
 int generateRandomNumber() {
   return (new Random()).nextInt(1000) + 1;
 }
 void handleClickEvent() {
   number.value = generateRandomNumber();
 }
 @override
 Widget build(BuildContext context) {
   return Column(
     mainAxisAlignment: MainAxisAlignment.center,
     children: [
       SizedBox(
         height: 40.0,
         child: Obx(
           () => Text(
             number.value == 0
                 ? 'Click in the button bellow to generate random number'
                 : "Random number: ${number}",
           ),
         ),
       ),
       ElevatedButton(
         onPressed: handleClickEvent,
         child: Text('Generate a number!'),
       ),
     ],
   );
 }
}

Note que nesse exemplo nós envolvemos o widget Text dentro de outro, o Obx, um widget do GetX para que possamos ouvir os observables.

Já, se queremos separar a lógica da view, ou se queremos passar uma informação adiante, o ideal é usar o Gerenciamento Reativo, isso porque o Get usa esse paradigma (programação reativa) em sua composição, e utiliza de streams para controlar os estados de maneira assíncrona.

É assim que ele sabe se um estado mudou ou não, e daí decide se deve ou não atualizar a UI, trazendo performance e certeza de que o estado realmente mudou.

Gerenciamento Reativo em prática

Como eu ia dizendo, vamos usar a maneira Reativa para separar a lógica das views, a UI ficou assim usando o Controller:

class Example extends StatelessWidget {
 final controller = Get.put(ControllerExample());
 @override
 Widget build(BuildContext context) {
   return Column(
     mainAxisAlignment: MainAxisAlignment.center,
     children: [
       SizedBox(
         height: 40.0,
         child: Obx(
           () => Text(
             controller.number.value == 0
                 ? 'Click in the button bellow to generate random number'
                 : "Random number: ${controller.number}",
           ),
         ),
       ),
       ElevatedButton(
         onPressed: controller.handleClickEvent,
         child: Text('Generate a number!'),
       ),
     ],
   );
 }
}

Podemos observar que nosso controller é inicializado assim que nosso Widget é usado.

final controller = Get.put(ControllerExample());

E ao longo da UI usamos o controller para saber o estado da variável number, o que nos traz o mesmo comportamento dos exemplos anteriores, mas na execução é possível ver o funcionamento do Obx utilizado escopar o estado number, assim temos o resultado esperado:

Como podemos ver, o número na AppBar não atualizou! Com o Obx ouvindo o number, apenas a parte que necessitamos atualizar é renderizada novamente.
Para finalizar, imagine que precisamos desse número gerado em outra tela, como faríamos? O Get têm uma sintaxe simples e “meio mágica”, ele consegue identificar o controller, uma vez que instanciado, na memória e assim recuperar todo o contexto e estado com o Get.find.

final ControllerExample controller = Get.find();

Então, em uma segunda tela usaremos o nosso estado number:

class PageThatNeedsNumberOfExample extends StatelessWidget {
 final ControllerExample controller = Get.find();
 
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: Text('Segunda tela'),
     ),
     body: Container(
       child: Center(
         child: Text(controller.number.value == 0
             ? "Você ainda não gerou um número :)"
             : "O número random foi: ${controller.number}"),
       ),
     ),
   );
 }
}

Antes de qualquer coisa, precisamos de uma navegação para a segunda tela, com o Get podemos usar o Get.to e indicar para qual tela desejamos ir

ElevatedButton(
               child: Text('Next Route'),
               onPressed: () {
                 Get.to(PageThatNeedsNumberOfExample());
               },
             ),

Mas, isso não vai funcionar, pois, o Get não tem conhecimento das rotas que o MaterialApp cria, para isso precisamos indicar qual navigator estamos usando (navigatorKey), e nesse caso usaremos o do Get:

 

class MyApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     navigatorKey: Get.key,
     title: 'Flutter Demo',
     theme: ThemeData(
       primarySwatch: Colors.blue,
     ),
     home: MyHomePage(title: 'Flutter Demo Home Page'),
   );
 }
}

Pronto! Nosso App funciona e já conseguimos passar o estado adiante, sem muito esforço. Esse é o resultado:

Concluindo, sinta-se livre para testar os diversos gerenciadores de estado que existem, mas não tenha medo do setState, que muitas vezes é demonizado, o Flutter evoluiu bastante desde que o setState causava muitos problemas, obrigado por ler e até uma próxima! 🙂

Aproveite para acompanhar outros posts de tutoriais que temos aqui no blog da Taller. 🔥