Objeto: criação de um pequeno app com gerência de estado e comunicação entre componentes gráficos.
Principais termos técnicos abordados: gerência de estados, ValueNotifier, flutter hooks, useState, injeção de dependência
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
"Uma interface burra é uma interface inquebrável" (Algum engenheiro de software, acho que Tom DeMarco)
Mais gerência de estados.
Vamos trabalhar e avançar sobre o código da receita anterior, sem muita conversa na introdução.
Crie um novo projeto, adicione a biblioteca extra necessária, o flutter hooks, e cole o código a seguir no main.dart. Se não souber do que estou falando, remeta-se à receita anterior. Você também pode partir do código que fez na receita anterior.
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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 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))
]);
}
}
class DataTableWidget extends StatelessWidget {
final List jsonObjects;
final List<String> columnNames;
final List<String> propertyNames;
DataTableWidget( {this.jsonObjects = const [], this.columnNames = const ["Nome","Estilo","IBU"], this.propertyNames= const ["name", "style", "ibu"]});
@override
Widget build(BuildContext context) {
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());
}
}
A interface é essa aqui...
O que temos de mais interessante até agora no código:
um widget genérico para exibir tabelas, o DataTableWidget.
um widget nem tão genérico, o NewNavBar, mas que gerencia seu estado internamente para deixar selecionado o item que o usuario tocar
uma variável global, dataObjects, que é passada para a fábrica de DataTableWidget para que esta monte a tabela devidamente.
No entanto, quando o usuário toca nos botões Cafés, Cervejas e Nações nada acontece além do botão ficar selecionado.
O NewNavBar lida decentemente com a mudança de estado interno dele, mas não parece ser muito da conta dele mexer com o componente que mostra a tabela, o DataTableWidget.
Maaaas, se a gente olhar bem, dá pra perceber que a fábrica de DataTableWidget recebe aquela variável global dataObjects como parâmetro.
E variável, global, como a gente sabe, está acessível em todo canto, inclusive no NewNavBar...
- Já sei, já sei! A gente pode modificar o dataObjects quando um botão for tocado no NewNavBar! Daí como o DataTableWidget é construído com base no dataObjects, ele vai exibir os dados que a gente botar...
Ok. Vamos tentar isso.
Modifique seu NewNavBar.
class NewNavBar extends HookWidget {
NewNavBar();
@override
Widget build(BuildContext context) {
var state = useState(1);
return BottomNavigationBar(
onTap: (index){
state.value = index;
dataObjects = [{
"name": "La Fin Du Monde",
"style": "Bock",
"ibu": "65"
},
{
"name": "Sapporo Premiume",
"style": "Sour Ale",
"ibu": "54"
},
{
"name": "Duvel",
"style": "Pilsner",
"ibu": "82"
}
];
},
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))
]);
}
}
Agora salve, rode e acione os botões da barra de navegação.
- Ah, professor, deu bode... aconteceu nada não...
Pois bem. Aconteceu alguma coisa, só não aconteceu o que a gente queria.
- E o que aconteceu?
Aconteceu que você modificou uma variável global, não deve ser o feito mais glorioso de sua vida.
- Mas aquela tabela não é construída com base no dataObjects? Eu alterei o dataObjects...
É sim. Só que quando você alterou o dataObjects o DataTableWidget já tinha sido montado e renderizado há tempos. E não há nada, por enquanto, que conecte uma mudança nesse dataObjects com uma reconstrução do seu DataTableWidget.
- E o que a gente precisa fazer?
A gente precisa, basicamente, transformar o dataObjects num estado e conectar o DataTableWidget a esse estado.
- Dá pra usar o useState como na receita passada?
Não, não dá. O useState serve para estados internos a um componente, para que mudanças no estado interno (como um índice selecionado) daquele componente onde você chama o useState desencadeiem a reconstrução daquele próprio componente. No entando, aqui estamos falando de uma interação num componente (toque em item da barra de navegação) desencadear ações que terminem reconstruindo outro componente (a tabela).
O buraco é mais "inbaixo".
A gente já sabe que aquela variável dataObjects é candidatíssima a ser estado do app. E não dá pra ser estado interno do DataTableWidget porque outros componentes precisam mexer nesse estado, e componentes não mexem em estados internos de outros componentes. Tem que ser um estado global, digamos assim. Ou, ao menos, um estado visível a todos os componentes envolvidos - nesse caso, a barra de navegação e a tabela.
Mantendo a analogia da receita anterior, a gente poderia simplesmente fazer um "twitter" para aquela variável dataObjects e botar o DataTableWidget pra seguir esse "twitter". Assim, a cada modificação que fizéssemos, o DataTableWidget seria remontado e renderizado.
- Eita, e dá pra usar o useState pra fazer esse twitter pra gente?
Não, caraio, o useState é tão somente para estados internos. Vamos ter que fazer o twitter nós mesmos, na tora.
- E como?
Bem, há umas 20 maneiras de fazer isso, com diversas bibliotecas nem sempre muito fáceis de entender ou usar. Nós vamos abordar a solução mais simples e nativa, sem importar nenhum pacote extra. Ela serve perfeitamente para aplicativos mais simples.
Crie um twitter para o seu dataObjects. Deixe o seu código inicial assim.
final ValueNotifier<List> twitter = new ValueNotifier([]);
//var dataObjects = [];
- Vige, só isso?
Só. Não é incrível?
Veja que comentei o dataObjects. Ele vai "viver" dentro do twitter agora. O ValueNotifier é uma classe cujos objetos podem armazenar um valor (adivinhe só - o estado) e sabem notificar "seguidores" caso esse valor venha a ser modificado.
- E aquele <List>, é o que?
É que os objetos ValueNotifier são capazes de guardar dentro de si qualquer tipo de valor: booleanos, inteiros, Strings, objetos JSON etc. Como queremos guardar dentro dele o dataObjects e o dataObjects é um List, declaramos daquela forma.
- Beleza, e se eu quiser "publicar" nesse twitter.
Simples, basta fazer twitter.value = alguma_coisa, do mesmo jeitinho que a gente fazia lá no useState. É que a gente não viu, mas o useState retorna um ValueNotifier.
Então vamos publicar nesse twitter.
Altere seu NewNavBar...
class NewNavBar extends HookWidget {
NewNavBar();
@override
Widget build(BuildContext context) {
var state = useState(1);
return BottomNavigationBar(
onTap: (index){
state.value = index;
twitter.value = [{
"name": "La Fin Du Monde",
"style": "Bock",
"ibu": "65"
},
{
"name": "Sapporo Premiume",
"style": "Sour Ale",
"ibu": "54"
},
{
"name": "Duvel",
"style": "Pilsner",
"ibu": "82"
}
];
},
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))
]);
}
}
Bem simples, mas ainda temos um erro de compilação para corrigir, uma funcionalidade para acrescentar e uma seboseira para limpar.
Primeiro vamos ao erro de compilação.
Passávamos para a fábrica do nosso DataTableWidget aquele dataObjects. Sendo que ele não existe mais.
Nosso primeiro instinto seria simplesmente trocar as linhas...
(...)
body: DataTableWidget(
jsonObjects: dataObjects,
propertyNames: ["name","style","ibu"],
columnNames: ["Nome", "Estilo", "IBU"]
)
(...)
... por...
(...)
body: DataTableWidget(
jsonObjects: twitter.value,
propertyNames: ["name","style","ibu"],
columnNames: ["Nome", "Estilo", "IBU"]
)
(...)
O app vai rodar mas, infelizmente, não vai dar em nada. Porque passar o twitter.value para um DataTableWidget não é suficiente para inscrevê-lo no twitter. A solução é usar um widget que saiba se inscrever no twitter e se redesenhar a cada nova publicação e empurrar o DataTableWidget dentro deste widget que sabe seguir o twitter. Assim, por tabela, como o DataTableWidget está dentro dele, vai ser redesenhado também quando "publicarem" alguma coisa nova. Esse widget bisbilhoteiro de twitter existe, e é bem fácil de usar e aceita qualquer outro widget dentro dele.
Tudo o que a gente quer!
Modifique a classe MyApp (que chama a fábrica do DataTableWidget).
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: ValueListenableBuilder(
valueListenable: twitter,
builder:(_, value, __){
return DataTableWidget(
jsonObjects:value,
propertyNames: ["name","style","ibu"],
columnNames: ["Nome", "Estilo", "IBU"]
);
}
),
bottomNavigationBar: NewNavBar(),
));
}
}
Execute e veja a mágica acontecendo.
Aparecem as cervejas devidas na tabela ao toque no botão Cervejas...
E não é tão complicado assim de entender. Dê uma sacada nesse código:
(...)
body: ValueListenableBuilder(
valueListenable: twitter,
builder:(_, value, __){
return DataTableWidget(
jsonObjects: value,
propertyNames: ["name","style","ibu"],
columnNames: ["Nome", "Estilo", "IBU"]
);
}
)
(...)
O ValueListenableBuilder é a fábrica que vai criar um objeto que sabe seguir o tal twitter. A gente precisa passar dois parâmetros para essa fábrica guardar com carinho no objeto que ela está criando. O primeiro parâmetro, valueListenable, é mais que óbvio: o "twitter" que o objeto deve "seguir". O segundo parâmetro nomeado, builder, é uma função de construção, como o build dos nossos StatelessWidget. E, como o build, o builder, como função, deve retornar um widget - no caso, o nosso DataTableWidget. Sendo que esse builder recebe 3 parâmetros e um deles é justamente a "publicação" feita no twitter, que a gente tá identificando aqui por value mas você pode chamar até de cachorra se você quiser. Estou chamando os outros dois parâmetros de _ e __ porque não vamos usá-los e designá-los assim melhora a legibilidade da função - indica que você deve ignorá-los. E estou chamando de value o parâmetro do meio porque ele será precisamente o valor do twitter.value, que é nossa ilustre lista de objetos JSON representando cervejas. Assim, o ValueListenableBuilder tem um estado, e chama esse método builder para redesenhar-se a cada vez que o valor do estado muda e passando esse valor como parâmetro para a função de construção. A gente não faz nada desse "trabalho sujo".
E por falar em trabalho sujo, temos sujeira para limpar, lembra?
A metáfora talvez seja até útil, mas claro que é uma presepada da porra deixar uma variável com o nome de twitter. Essa variável tem o valor do estado da tabela dentro de si, então state seria um nome mais apropriado. Ou tableState ou tableStateNotifier, já que o estádo não é esse objeto, mas está dentro dele, no atributo value.
Então troque as ocorrências do nome twitter por tableStateNotifier.
Eu vou admitir que você consegue fazer esse trabalho de alta carga cognitiva sem minha ajuda, não me decepcione.
- Beleza, professor. Era só essa a sujeira?
Não. Tem bem mais. Veja, esse componente eminentemente gráfico...
class NewNavBar extends HookWidget {
NewNavBar();
@override
Widget build(BuildContext context) {
var state = useState(1);
return BottomNavigationBar(
onTap: (index){
state.value = index;
tableStateNotifier.value = [{
"name": "La Fin Du Monde",
"style": "Bock",
"ibu": "65"
},
{
"name": "Sapporo Premiume",
"style": "Sour Ale",
"ibu": "54"
},
{
"name": "Duvel",
"style": "Pilsner",
"ibu": "82"
}
];
},
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))
]);
}
}
Agora reflita: por que djabo esse componente está sabendo os nomes e todas as informações das cervejas que devem ser exibidas EM OUTRO COMPONENTE??? Esse componente não era pra saber nem os ícones e labels que renderiza, porque isso poderia vir como parâmetro para a fábrica e o widget ficaria reutilizável. Mas, por ora, temos que tirar esse "conhecimento" daí. Interface com o usuário tem que ser tão burra quanto possível, como o nosso DataTableWidget, que é capaz de montar tabela do que você bem entender, desde que você passe os parâmetros corretos para sua fábrica.
Uma primeira opção seria criar uma função fora das classes, uma função que modificasse o estado com os valores das cervejas. Mais adiante, a gente pode usar essa função para ir buscar tudo isso numa API na Internet. Por enquanto, eu quero apenas burrificar minha interface gráfica pra limpar seboseira do nosso código. Não entenda mal o burrificar. Interface gráfica pode ser sabidinha, mas apenas em relação a questões de interface gráfica, o que não está sendo o caso aqui, onde dados estão brotando de um componente gráfico.
Acrescente uma função logo abaixo do ValueNotifier.
final ValueNotifier<List> tableStateNotifier = new ValueNotifier([]);
void carregarCervejas(){
tableStateNotifier.value = [{
"name": "La Fin Du Monde",
"style": "Bock",
"ibu": "65"
},
{
"name": "Sapporo Premiume",
"style": "Sour Ale",
"ibu": "54"
},
{
"name": "Duvel",
"style": "Pilsner",
"ibu": "82"
}
];
}
Agora acrescente a chamada a essa função a partir do NewNavBar...
class NewNavBar extends HookWidget {
NewNavBar();
@override
Widget build(BuildContext context) {
var state = useState(1);
return BottomNavigationBar(
onTap: (index){
state.value = index;
carregarCervejas();
},
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))
]);
}
}
- E agora, limpou a seboseira?
De jeito nenhum. Na verdade, como diria um amigo meu do sítio, e ele diria de maneira bem menos sutil: isso é o mesmo que ter uma diarreia e limpar o monossílabo com canjica.
- Mas por quê?
Porque nosso componente NewNavBar está amarrado àquela função carregarCervejas que por sua vez está amarrada a uma variável global que é o tableStateNotifier. Assim, nosso componente gráfico só funciona se aquela variável global existir, o que é uma roubada.
- E como a gente faz pra resolver?
Bem, dentro dos objetos a gente pode ter informação e função, não é mesmo? Solucionando com o que a gente já estudou até aqui, eu guardaria dentro do objeto NewNavBar uma referência para uma função de callback e chamaria essa função no lugar da função específica carregarCervejas. Isso transfere a responsabilidade de saber qual função será invocada para quem construir os objetos NewNavBar. É como se a fábrica de NewNavBar dissesse: "me diga quem que você quer que eu chame que eu chamo, mas eu não quero saber mais nada a esse respeito".
Então modifique a classe NewNavBar...
class NewNavBar extends HookWidget {
var itemSelectedCallback; //esse atributo será uma função
NewNavBar({this.itemSelectedCallback}){
itemSelectedCallback ??= (){} ;
}
@override
Widget build(BuildContext context) {
var state = useState(1);
return BottomNavigationBar(
onTap: (index){
state.value = index;
itemSelectedCallback();
//carregarCervejas();
},
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))
]);
}
}
A nossa função de callback vai ser armazenada em...
var itemSelectedCallback;
... e de quebra a gente se protege para o caso de quem chamar a fábrica não passar nenhuma função de callback. A linha...
itemSelectedCallback ??= (){};
... atribui uma função que não faz nada (mas não é nula e, por isso, não gera erro) ao itemSelectedCallback. Essa linha é uma forma legal e sintética de se escrever:
if (itemSelectedCallback == null) itemSelectedCallback = (){};
Agora modifique a chamada à fábrica de NewNavBar...
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: ValueListenableBuilder(
valueListenable: tableStateNotifier,
builder:(_, value, __){
return DataTableWidget(
jsonObjects:value,
propertyNames: ["name","style","ibu"],
columnNames: ["Nome", "Estilo", "IBU"]
);
}
),
bottomNavigationBar: NewNavBar(itemSelectedCallback: carregarCervejas),
));
}
}
Agora nossos objetos NewNavBar não dependem de nenhuma função ou variável global específica. Porque a função que vai ser chamada ao toque de qualquer de seus botões é uma função injetada na chamada da fábrica, no construtor NewNavBar. Quem for construir um objeto NewNavBar, que passe pra ele a função. O nome disso é injeção de dependência, e é coisa bem importante na engenharia de software. Vejam como já melhoramos nosso widget aplicando esse conceito. Ele ficou mais genérico e mais fácil de ser reutilizado, mais independente, quase um hippie.
Mas tem mais.
- Sempre tem, professor...
Sempre.
Tá meio que na cara que esse nosso widget NewNavBar vai ter que desencadear ações que terminem mostrando cervejas quando o cara toca no botão Cervejas, cafés quando o cara toca no botão Cafés e nações quando o cara toca no botão Nações.
Acontece que a função de callback, a itemSelectedCallback, não tem como saber em qual botão ocorreu o clique.
Só que "a gente" é quem chama a função de callback, de dentro do NewNavBar. E acontece que a gente sabe o índice do botão que foi tocado, então podemos repassar esse índice para a função de callback. A mudança vai ser sutil.
Edite o NewNavBar.
class NewNavBar extends HookWidget {
var itemSelectedCallback;
NewNavBar({this.itemSelectedCallback}){
itemSelectedCallback ??= (_){} ;
}
@override
Widget build(BuildContext context) {
var state = useState(1);
return BottomNavigationBar(
onTap: (index){
state.value = index;
itemSelectedCallback(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))
]);
}
}
Note que mudamos quase nada e nosso NewNavBar continua "burro", sem querer saber de nada, apenas notificando a função de callback do índice do botão que foi tocado. A função de callback que tome as devidas providências. Vamos fazer isso acontecer.
Escreva uma função carregar, que recebe um inteiro. Se o inteiro for 1, chame a função carregarCervejas.
final ValueNotifier<List> tableStateNotifier = new ValueNotifier([]);
void carregar(index){
if (index == 1) carregarCervejas();
}
void carregarCervejas(){
tableStateNotifier.value = [{
"name": "La Fin Du Monde",
"style": "Bock",
"ibu": "65"
},
{
"name": "Sapporo Premiume",
"style": "Sour Ale",
"ibu": "54"
},
{
"name": "Duvel",
"style": "Pilsner",
"ibu": "82"
}
];
}
Precisamos complementar isso.
Passe a nova função para a fábrica de NewNavBar. Muda quase nada.
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: ValueListenableBuilder(
valueListenable: tableStateNotifier,
builder:(_, value, __){
return DataTableWidget(
jsonObjects:value,
propertyNames: ["name","style","ibu"],
columnNames: ["Nome", "Estilo", "IBU"]
);
}
),
bottomNavigationBar: NewNavBar(itemSelectedCallback: carregar),
));
}
}
Pronto. Deve rodar direitinho.
- Ufa. Agora acabou?
Tem só mais uma besteirinha.
- Ai ai...
É que a gente está com uma ruma de função solta - tem a função carregar, a função carregarCervejas e vão aparecer também as funções carregarCafes e carregarNacoes. Essas funções chamam umas às outras, têm a ver umas com as outras e operam, essencialmente, sobre a variável tableStateNotifier. Só falta gritarem para fazerem parte de uma mesma classe. Classe é uma ruma de função que faz operações relacionadas processando variáveis em comum. Vamos ajeitar mais essa.
Troque todo o código abaixo dos imports e acima do main por isso aqui.
class DataService{
final ValueNotifier<List> tableStateNotifier = new ValueNotifier([]);
void carregar(index){
if (index == 1) carregarCervejas();
}
void carregarCervejas(){
tableStateNotifier.value = [{
"name": "La Fin Du Monde",
"style": "Bock",
"ibu": "65"
},
{
"name": "Sapporo Premiume",
"style": "Sour Ale",
"ibu": "54"
},
{
"name": "Duvel",
"style": "Pilsner",
"ibu": "82"
}
];
}
}
final dataService = DataService();
Pronto, agora as coisas estão mais orientadas a objetos. Basicamente, apenas movemos o código que já estava escrito pra uma nova classe e criamos um objeto desta classe como variável global. Precisamos de mais um ou outro ajuste, apenas.
Modifique a chamada à fábrica de ValueListenableBuilder e a chamada à fábrica de NewNavBar.
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: ValueListenableBuilder(
valueListenable: dataService.tableStateNotifier,
builder:(_, value, __){
return DataTableWidget(
jsonObjects:value,
propertyNames: ["name","style","ibu"],
columnNames: ["Nome", "Estilo", "IBU"]
);
}
)
,
bottomNavigationBar: NewNavBar(itemSelectedCallback: dataService.carregar),
));
}
}
Pronto, nosso app está mais redondinho. Se você fez os exercícios anteriores, já sabe deixar o NewNavBar bem mais genérico, recebendo ícones e labels na fábrica em vez de deixar tudo fixo.
- Tá, mas agora acabou, né?
Calma, tem os exercícios ainda ;)
ValueNotifier - documentação oficial e uma playlist do fluterando (sobre ValueNotifier e afins). Apreoveite e dê uma olhada no canal, tem muita coisa boa e em português.
Injeção de dependência - texto simples, em inglês.
Funções como variáveis - chave de pesquisa sobre o assunto.
Já armei tudo para você povoar essa tabela com cafés quando o cara aciona o botão Cafés e com nações quando o cara aciona o botão Nações. Então faça isso. Inicialmente, deixe os mesmos nomes de colunas e de propriedades dos objetos JSON. Invente nomes e outros valores para cafés e nações.
(desafio)Sua solução para a função carregar deve ter envolvido if's ou switch/case. Faça uma solução sem nenhum desses comandos, preservando a função com o mesmo parâmetro, sem acrescentar nada.
Agora continue na questão 1, sendo que os nomes das colunas e os nomes das propriedades exibidas no DataWidget devem mudar conforme se estejam carregando cafés ou nações. Clique nos links se quiser objetos JSON de cafés, nações e cervejas.