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:
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!
A ideia é fazer o usuário clicar no botão para obter um novo número, e este é o resultado:
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:
Depois do click:
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:
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. 🔥