Objeto: criação de um componente gráfico com gerência de estado.
Principais termos técnicos abordados: flutter hooks, useState, programação imperativa, programação descritiva
Requisitos para as instruções funcionarem: navegador ou ide (VS Code, Android Studio etc.)
Requisitos para compreensão das instruções: noções básicas de programação e ter realizado receita anterior, com exercícios
Como ler essa receita: instruções, dentro dos passos da receita e que requerem ação sua no computador, estarão escritas em cor azul. Comentários sobre os passos estarão em fonte normal, cor preta. Comandos, código-fonte, termos técnicos ou configuração explícita, estarão com fonte diferenciada.
Observação: eventuais instruções de terminal serão comandos Linux. Adapte caso esteja usando outro sistema operacional
"Minha mulher pode brigar comigo porque eu sou preguiçoso, porque eu bebo muito ou porque eu não ajudo em casa. Mas, normalmente, ela briga comigo porque sim, e isso é muito errado" (Ricardin)
Gerência de estados.
Esse é um assunto extemamente delicado, importante e, sobretudo, negligenciado.
Numa frase só, o estado de um aplicativo são os dados que seus componentes gráficos processam para serem montados e renderizados. E a gerência de estados, continuando numa frase só, é a automatização da recostrução dos componentes gráficos devidos diante de mudanças nesses dados.
Dê uma olhada na cara desse app que já lhe deve ser familiar e pense em que dados poderiam fazer parte de seu estado, ou seja, pense sobre que dados são usados para a construção e renderização dos componentes gráficos.
Vamos lá. Há dados que são descaradamente estado do app, há dados que são sutilmente estado do app, e há dados que achamos que não são, mas poderiam ou deveriam ser.
Descaradamente, os dados das cervejas fazem parte do estado. Construímos a tabela central com base nesses dados. Mais que isso: se eles mudarem, o componente central, a tabela, precisa redesenhar-se. De uma forma bem mais discreta, a coluna de ordenação deve ser parte do estado também pois, caso queiramos ordenar a tabela por alguma coluna, isso deve estar indicado graficamente (uma seta no cabeçalho da coluna). O mesmo vale para a informação sobre se a ordenação é ascendente ou descendente (seta pra cima, seta pra baixo).
Ao menos o índice do botão selecionado na barra de navegação de baixo também deve fazer parte do estado do app, pois a cor do botão selecionado fica diferente da cor dos demais. Ou seja: esse índice tem efeito direto para a construção da interface gráfica. Eu vou além disso: se você for um pouco esperto, você faz um componente genérico para essa barra de navegação e transfere para o estado do app os labels e objetos IconData necessários para a construção da barra de navegação.
- Ah professor, mas esses botões da barra de navegação nunca mudam, precisam ser estado não...
Eu não escrevi que dados precisam mudar para serem estado do app. Eu escrevi que precisam ser processados para a contrução da interface gráfica. Se não mudarem nunca mas algum build no meio do caminho usá-los para montar algum componente gráfico, sugiro que empurre-os no estado do app. Ademais, esses dados, especificamente, não mudam apenas por enquanto. Você pode querer internacionalizar seu app, adicionar novos idiomas, e aqueles labels precisariam mudar, eventualmente.
Eventuais mudanças no estado de um aplicativo podem ocorrer porque algum dado solicitado pelo usuário chegou através de requisição via internet, porque o usuário solicitou apagar alguma linha de tabela ou tão simplesmente "porque sim". Num app de apostas, por exemplo, basta o tempo passar sem que seu usuário faça absolutamente nada para que as odds das apostas mudem e você precise reconstruir ou atualizar rapidamente sua interface. Num app de rede social, como 'Zap' ou Facebook, a mesma coisa: novas publicações chegam à interface de seu usuário sem nenhuma interação direta dele.
Um app que reconstrói rapidamente sua interface gráfica após mudança no estado do software é dito um app responsivo. Isso é um requisito para, virtualmente, qualquer software comercial que você venha a desenvolver em qualquer plataforma, então é de se imaginar que a gente precise de ajuda externa para isso.
De fato, há diversas bibliotecas/pacotes para isso. React, Angular e Vue, são exemplos do mundo JavaScript/Web. Em Dart, temos um punhado de opções também. Nessa receita, vamos apenas iniciar a brincadeira.
Bem, já falamos bastante sobre o que um estado de app é. Precisamos saber como representá-lo, onde colocá-lo e como reagir automaticamente (ou quase) a eventuais mudanças.
Nesta receita, vamos nos concentrar na barra de navegação inferior para tratar da gerência de estados.
Crie um novo projeto e cole esse código no main.dart.
import 'package:flutter/material.dart';
var dataObjects = [];
void main() {
MyApp app = MyApp();
runApp(app);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.deepPurple),
debugShowCheckedModeBanner:false,
home: Scaffold(
appBar: AppBar(
title: const Text("Dicas"),
),
body: DataTableWidget(jsonObjects:dataObjects),
bottomNavigationBar: NewNavBar(),
));
}
}
class NewNavBar extends StatelessWidget {
NewNavBar();
void buttonTapped(int index) {
print("Tocaram no botão $index");
}
@override
Widget build(BuildContext context) {
return BottomNavigationBar(onTap: buttonTapped, items: const [
BottomNavigationBarItem(
label: "Cafés",
icon: Icon(Icons.coffee_outlined),
),
BottomNavigationBarItem(
label: "Cervejas", icon: Icon(Icons.local_drink_outlined)),
BottomNavigationBarItem(label: "Nações", icon: Icon(Icons.flag_outlined))
]);
}
}
class DataTableWidget extends StatelessWidget {
final List jsonObjects;
DataTableWidget( {this.jsonObjects = const [] });
@override
Widget build(BuildContext context) {
var columnNames = ["Nome","Estilo","IBU"],
propertyNames = ["name", "style", "ibu"];
return DataTable(
columns: columnNames.map(
(name) => DataColumn(
label: Expanded(
child: Text(name, style: TextStyle(fontStyle: FontStyle.italic))
)
)
).toList()
,
rows: jsonObjects.map(
(obj) => DataRow(
cells: propertyNames.map(
(propName) => DataCell(Text(obj[propName]))
).toList()
)
).toList());
}
}
Vamos analisar partes específicas do código mas, antes, rode e veja como está o app...
Note que não mostramos os dados das cervejas da tabela. Eles voltarão do jeito certo e no momento certo.
Comecemos analisando a parte mais besta do código, a classe...
class NewNavBar extends StatelessWidget {
NewNavBar();
void buttonTapped(int index) {
print("Tocaram no botão $index");
}
@override
Widget build(BuildContext context) {
return BottomNavigationBar(onTap: buttonTapped, items: const [
BottomNavigationBarItem(
label: "Cafés",
icon: Icon(Icons.coffee_outlined),
),
BottomNavigationBarItem(
label: "Cervejas", icon: Icon(Icons.local_drink_outlined)),
BottomNavigationBarItem(label: "Nações", icon: Icon(Icons.flag_outlined))
]);
}
}
A gente acrescentou um método para ser invocado quando o usuário toca no botão, que é o método buttonTapped. Quem invoca esse método não é a gente, mas nos faz o favor de passar o índice do botão acionado como parâmetro. O que a gente precisa é fazer algo de útil com isso, com esse dado, com esse índice, porque aquele print lá dentro do método, definitivamente, não é nada de útil.
Vamos ver porque "índice de botão tocado" é coisa importante.
Modifique a classe NewNavBar e execute o app...
class NewNavBar extends StatelessWidget {
NewNavBar();
void buttonTapped(int index) {
print("Tocaram no botão $index");
}
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
onTap: buttonTapped,
currentIndex: 1,
items: const [
BottomNavigationBarItem(
label: "Cafés",
icon: Icon(Icons.coffee_outlined),
),
BottomNavigationBarItem(
label: "Cervejas", icon: Icon(Icons.local_drink_outlined)),
BottomNavigationBarItem(label: "Nações", icon: Icon(Icons.flag_outlined))
]);
}
}
A interface está aqui, atente para a barra de navegação de baixo...
Veja quanta emoção: esse componente BottomNavigationBar recebeu um parâmetro currentIndex com valor 1 e renderizou o botão de índice 1 ("Cervejas") selecionado (pintado de roxo). Nesse momento a gente se sente tentado a pensar que, se a gente conseguir por uma referência para o BottomNavidationBar numa variável x, bastará fazer...
x.currentIndex = 0;
ou
x.currentIndex = 2;
para as coisas mudarem magicamente no componente gráfico.
No entanto, não é mais assim que as coisas, que essas coisas, ao menos, devem acontecer.
A gente pensa assim porque está acostumado com programação imperativa, e agora a gente tem que pensar em programação descritiva.
Eu não vou gastar byte aqui explicando porque a programação migrou para esse modelo descritivo, apenas aceite isso e entenda que você ficará bem defasado se insistir no modelo meramente imperativo.
E é justamente nesse modelo de programação descritiva que vai entrar o nosso glorioso estado.
A gente vai criar um estado interno para esse componente da barra de navegação, fazer com que essa informação desse estado entre naquele parâmetro current index e fazer com que o componente se redesenhe quando o estado mudar.
- e o que tem de descritivo nisso, professor?
Bem, seu widget vai ser uma mera descrição de como montar a barra de navegação, sem nenhuma "inteligência". Como, aliás, esses widgets já são. Precisamos que continuem assim.
Vamos usar um pacote (ou biblioteca) que nos ajudará a criar esse estado para nosso componente NewNavBar, que é o pacote flutter hooks.
Adicione essa biblioteca ao seu projeto. No console, de dentro do diretório de seu projeto, escreva:
flutter pub add flutter_hooks
Você pode conferir outras formas de instalar esse pacote aqui, mas essa forma que citei deve funcionar.
Istalado o pacote, podemos importá-lo.
Acrescente a seguinte linha na seção de importação.
import 'package:flutter_hooks/flutter_hooks.dart';
Nesse pacote, os estados podem ser associados aos widgets, e não ao aplicativo como um todo.
Altere sua classe NewNavBar...
class NewNavBar extends HookWidget {
NewNavBar();
void buttonTapped(int index) {
print("Tocaram no botão $index");
}
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
onTap: buttonTapped,
currentIndex: 1,
items: const [
BottomNavigationBarItem(
label: "Cafés",
icon: Icon(Icons.coffee_outlined),
),
BottomNavigationBarItem(
label: "Cervejas", icon: Icon(Icons.local_drink_outlined)),
BottomNavigationBarItem(label: "Nações", icon: Icon(Icons.flag_outlined))
]);
}
}
Com isso, estendendo HookWidget em vez de StatelessWidget, seu componente NewNavBar é passível de ter um estado. Mas não tem ainda.
Vamos resolver isso.
Edite seu widget.
class NewNavBar extends HookWidget {
NewNavBar();
void buttonTapped(int index) {
print("Tocaram no botão $index");
}
@override
Widget build(BuildContext context) {
var state = useState(1);
return BottomNavigationBar(
onTap: buttonTapped,
currentIndex: state.value,
items: const [
BottomNavigationBarItem(
label: "Cafés",
icon: Icon(Icons.coffee_outlined),
),
BottomNavigationBarItem(
label: "Cervejas", icon: Icon(Icons.local_drink_outlined)),
BottomNavigationBarItem(label: "Nações", icon: Icon(Icons.flag_outlined))
]);
}
}
Isso. Duas míseras linhazinhas e a gente tem um estado. Falta modificá-lo, ainda, mas vamos analisar o que temos.
A linha...
var state = useState(1);
...cria um estado para esse seu componente. O estado nada mais é do que um objeto com um valor lá dentro de si, armazenado numa propriedade identificada por value. O valor inicial de state.value vai ser 1 porque a gente passou 1 como parâmetro pro método useState. Parece estranho, mas o valor do estado vai "sobreviver" a re-renderizações do componente - o pacote vai garantir isso pra gente. Parece estranho porque está sendo atribuído o resultado de uma chamada de método a uma variável local. O instinto nos diz que o valor daquilo ali evapora depois que a execução do método build finaliza. Mas vamos ver como a coisa funciona.
Na linha...
currentIndex: state.value
...estamos passando justamente o valor do estado para a fábrica do BottonNavigationBar, através do parâmetro currentIndex.
Rode o app.
- Ah, professor, aconteceu nada. Eu toco nos botões e só fica selecionado o da Cerveja...
Isso acontece porque em momento algum nós estamos modificando o valor do estado. E a gente já sabe quando precisa modificar o valor do estado, não é mesmo?
- Quando o usuário toca no botão!
Precisamente.
- Mas a gente tá chamando o método buttonTapped quando os botões são acionados e o state é uma variável local no método build. Tem como acessar o state de lá no buttonTapped?
Não. Por restrição do pacote que estamos usando, o estado só pode ser obtido através do método useState, que é o tal "hook" (um dos vários do pacote flutter hooks). E um método hook, como o useState, só pode ser chamado dentro de métodos build. Isso também é restrição do pacote.
- Agora torou dentro...
Não necessariamente. A gente pode se livrar dessa função e escrever uma função inline, ao estilo das arrow functions. Só não precisa ter uma seta porque não retorna nada. A função inline, escrita dentro do build, enxerga a variável local state.
Altere sua classe.
class NewNavBar extends HookWidget {
NewNavBar();
@override
Widget build(BuildContext context) {
var state = useState(1);
return BottomNavigationBar(
onTap: (index){
state.value = index;
},
currentIndex: state.value,
items: const [
BottomNavigationBarItem(
label: "Cafés",
icon: Icon(Icons.coffee_outlined),
),
BottomNavigationBarItem(
label: "Cervejas", icon: Icon(Icons.local_drink_outlined)),
BottomNavigationBarItem(label: "Nações", icon: Icon(Icons.flag_outlined))
]);
}
}
Pois bem. BottomNavigationBar é uma fábrica. Cria objetos e põe coisas que a gente passa como parâmetro dentro deles. Dentro de objetos a gente pode por informações ou funções, e onTap é uma função. A gente está fazendo a mesma coisa que estávamos fazendo, passando a definição da função e não a chamada da função, sendo que estamos definindo a função ali mesmo e não separadamente. Quem vai chamar essa função é o componente gráfico - ela é a chamada função de callback, assunto de que já tratamos anteriormente.
Agora rode seu app e veja a mágica acontecendo...
- tá bom, entendi que funciona, mas ainda não entendi como.
Vamos lá.
Seu app roda dentro do framework flutter, uma espécie de biblioteca. O flutter é quem renderiza esses componentes todos que você escreve. Você nunca chamou nenhum método build que você escreveu e, no entanto você vê o resultado de chamadas a esses métodos na interface do seu app. Isso acontece porque é o flutter os invoca e dá um jeito de renderizar os widgets que eles retornam. Há meios de sinalizar ao flutter para redesenhar um componente e o pacote flutter hooks oferece essa função useState para nos ajudar.
O que acontece com esse app é bem simples, na verdade. Na primeira vez que o flutter renderiza o NewNavBar, o build é chamado. Dentro do build, o useState é invocado e, como é a primeira renderização, o estado é criado para o componente e com valor 1, que a gente passou como parâmetro...
var state = useState(1);
Quando você aciona o botão, o state.value é alterado, sendo que o método useState já fez as configurações necessárias para sinalizar ao flutter para redesenhar o componente em caso de mudança no state.value. Assim, se você escolher o botão de índice 2, por exemplo, esse método build vai ser chamado novamente, sendo que o estado do componente sobrevive entre renderizações, o que faz com que o retorno à mesmíssima chamada...
var state = useState(1);
retorne um objeto state com o value 2.
- tem como explicar com uma metáfora não? Gosto dessas exlicações técnicas não...
Bem, aquele state retornado pelo useState é como se fosse um twitter de um caba famoso, e o seu widget (NewNavBar) é como se fosse um Zé Mané como você que quer receber as besteiras que gente famosa escreve. O useState, antes de retornar o state, que é o twitter do caba famoso, põe o widget para segui-lo. Note que o state não está nem aí pro widget. O widget é que está interessado no que o state tem a publicar. Mexer no state.value, por fim, é como publicar alguma coisa no twitter do state: seu seguidor (apenas o widget, nesse caso), vai ser notificado. E vai ser redesenhado - seu método build vai ser chamado novamente pelo flutter.
- melhorou. Mas e pra mostrar diferentes dados quando diferentes botões forem acionados?
Aguarde as próximas receitas. Mas o assunto continua o mesmo: gerência de estados.
Documentação oficial do flutter hooks.
Uma boa e velha pesquisa no google.
Texto em português sobre hooks.
15 ou 16 opções diferentes para gerência de estados. Tem mais pacote de gerência de estados do que tipo de corno.
Na primeira linha do build de cada uma das classes que você criou, acrecente a linha print("no build da classe X"), subistituindo o X pelo nome da classe. Analize as mensagens print na aba de logs após a primeira renderização do app. Em seguida, acione os botões da barra inferior diversas vezes e analise os logs novamente. O que você pode concluir de suas observações?
Há algumas estratégias nativas do flutter para gerência de estados (o flutter hooks é um pacote desenvolvido por terceiros). A mais básica delas, que faz coisa semelhante ao useState, é o StatefulWidget. Ative seu desenrolômetro e desenvolva um componente NewNavBar2, que deve fazer o mesmo que o NewNavBar, mas usando StatefulWidget em vez de useState. Reflita sobre qual solução você achou mais interessante. Se você achou uma chatice o StatefulWidget, não se preocupe - essa deve ser a única vez que eu vou obrigar você a usar essa solução. Para ajudar no exercício, pesquise aqui.