Objeto: criação de um pequeno app com gerência de estado, comunicação entre componentes gráficos e acesso a api externa para carregar dados.
Principais termos técnicos abordados: bibliotecas, imports, constantes, information hiding, get/set, private/public, números mágicos
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
A gente vai tratar de alguns assuntos que parecem bem bestas, mas que não são. Na maior parte do tempo estaremos apenas limpando o código, mas vamos aproveitar a ocasião para acrescentar uma funcionalidade também, possibilitada justamente por essa limpeza de código.
Vamos partir de um código conhecido. Cole-o e execute-o.
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
enum TableStatus{idle,loading,ready,error}
enum ItemType{beer, coffee, nation, none}
class DataService{
final ValueNotifier<Map<String,dynamic>> tableStateNotifier
= ValueNotifier({
'status':TableStatus.idle,
'dataObjects':[],
'itemType': ItemType.none
});
void carregar(index){
final funcoes = [carregarCafes, carregarCervejas, carregarNacoes];
funcoes[index]();
}
void carregarCafes(){
//ignorar solicitação se uma requisição já estiver em curso
if (tableStateNotifier.value['status'] == TableStatus.loading) return;
if (tableStateNotifier.value['itemType'] != ItemType.coffee){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': ItemType.coffee
};
}
var coffeesUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/coffee/random_coffee',
queryParameters: {'size': '10'});
http.read(coffeesUri).then( (jsonString){
var coffeesJson = jsonDecode(jsonString);
//se já houver cafés no estado da tabela...
if (tableStateNotifier.value['status'] != TableStatus.loading) coffeesJson = [...tableStateNotifier.value['dataObjects'], ...coffeesJson];
tableStateNotifier.value = {
'itemType': ItemType.coffee,
'status': TableStatus.ready,
'dataObjects': coffeesJson,
'propertyNames': ["blend_name","origin","variety"],
'columnNames': ["Nome", "Origem", "Tipo"]
};
});
}
void carregarNacoes(){
//ignorar solicitação se uma requisição já estiver em curso
if (tableStateNotifier.value['status'] == TableStatus.loading) return;
if (tableStateNotifier.value['itemType'] != ItemType.nation){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': ItemType.nation
};
}
var nationsUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/nation/random_nation',
queryParameters: {'size': '10'});
http.read(nationsUri).then( (jsonString){
var nationsJson = jsonDecode(jsonString);
//se já houver nações no estado da tabela...
if (tableStateNotifier.value['status'] != TableStatus.loading) nationsJson = [...tableStateNotifier.value['dataObjects'], ...nationsJson];
tableStateNotifier.value = {
'itemType': ItemType.nation,
'status': TableStatus.ready,
'dataObjects': nationsJson,
'propertyNames': ["nationality","capital","language","national_sport"],
'columnNames': ["Nome", "Capital", "Idioma","Esporte"]
};
});
}
void carregarCervejas(){
//ignorar solicitação se uma requisição já estiver em curso
if (tableStateNotifier.value['status'] == TableStatus.loading) return;
//emitir estado loading se items em exibição não forem cervejas
if (tableStateNotifier.value['itemType'] != ItemType.beer){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': ItemType.beer
};
}
var beersUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/beer/random_beer',
queryParameters: {'size': '10'});
http.read(beersUri).then( (jsonString){
var beersJson = jsonDecode(jsonString);
//se já houver cervejas no estado da tabela...
if (tableStateNotifier.value['status'] != TableStatus.loading) beersJson = [...tableStateNotifier.value['dataObjects'], ...beersJson];
tableStateNotifier.value = {
'itemType': ItemType.beer,
'status': TableStatus.ready,
'dataObjects': beersJson,
'propertyNames': ["name","style","ibu"],
'columnNames': ["Nome", "Estilo", "IBU"]
};
});
}
}
final dataService = DataService();
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: ValueListenableBuilder(
valueListenable: dataService.tableStateNotifier,
builder:(_, value, __){
switch (value['status']){
case TableStatus.idle:
return Center(child: Text("Toque em algum botão"));
case TableStatus.loading:
return Center(child: CircularProgressIndicator());
case TableStatus.ready:
return SingleChildScrollView(child: DataTableWidget(
jsonObjects: value['dataObjects'],
propertyNames: value['propertyNames'],
columnNames: value['columnNames']
)) ;
case TableStatus.error:
return Text("Lascou");
}
return Text("...");
}
),
bottomNavigationBar: NewNavBar(itemSelectedCallback: dataService.carregar),
));
}
}
class NewNavBar extends HookWidget {
final _itemSelectedCallback;
NewNavBar({itemSelectedCallback}):
_itemSelectedCallback = itemSelectedCallback ?? (int){}
@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))
]);
}
}
class DataTableWidget extends StatelessWidget {
final List jsonObjects;
final List<String> columnNames;
final List<String> propertyNames;
DataTableWidget( {this.jsonObjects = const [], this.columnNames = const [], this.propertyNames= const []});
@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 exeucução se dá mais ou menos assim:
A cada toque num item que já está sendo exibido, novos itens são carregados sem que se percar os que já estão lá. Há um problema de interface pois não estamos fazendo scroll horizontal, então algumas colunas não ficam visíveis.
Mas você vai resolver isso nos exercícios. Na receita, nos concentraremos em limpar a sujeira de nosso código.
E há bastante sujeira, então tome fôlego.
Comecemos pela parte mais gritante: todo o conteúdo do nosso app está num diretório só. Em dart, cada diretório desses é uma biblioteca (library), mesmo que você não queira. Então a gente tem uma biblioteca (o diretório lib) com componentes de interface gráfica (os widgets), componentes de acesso a dados e lógica de negócio (a classe DataService) e um programa principal.
É uma suruba muito grande, vamos organizar melhor as coisas.
Crie um diretório de nome data e um diretório de nome view no seu projeto, dentro do diretório onde está o main.dart. Dentro de data, crie um arquivo de nome data_service.dart. Dentro de view, crie um arquivo de nome widgets.dart.
Vou admitir, com certa preocupação, que você consegue executar essas instruções sem eu prover maiores detalhes.
Agora você vai tirar a classe DataService, as enumerações que ela usa e o objeto constante dataService do main.dart e transferir tudo para o data/data_service.dart. O conteúdo do arquivo data_service.dart fica assim:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
enum TableStatus{idle,loading,ready,error}
enum ItemType{beer, coffee, nation, none}
class DataService{
final ValueNotifier<Map<String,dynamic>> tableStateNotifier
= ValueNotifier({
'status':TableStatus.idle,
'dataObjects':[],
'itemType': ItemType.none
});
void carregar(index){
final funcoes = [carregarCafes, carregarCervejas, carregarNacoes];
funcoes[index]();
}
void carregarCafes(){
//ignorar solicitação se uma requisição já estiver em curso
if (tableStateNotifier.value['status'] == TableStatus.loading) return;
if (tableStateNotifier.value['itemType'] != ItemType.coffee){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': ItemType.coffee
};
}
var coffeesUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/coffee/random_coffee',
queryParameters: {'size': '10'});
http.read(coffeesUri).then( (jsonString){
var coffeesJson = jsonDecode(jsonString);
//se já houver cafés no estado da tabela...
if (tableStateNotifier.value['status'] != TableStatus.loading) coffeesJson = [...tableStateNotifier.value['dataObjects'], ...coffeesJson];
tableStateNotifier.value = {
'itemType': ItemType.coffee,
'status': TableStatus.ready,
'dataObjects': coffeesJson,
'propertyNames': ["blend_name","origin","variety"],
'columnNames': ["Nome", "Origem", "Tipo"]
};
});
}
void carregarNacoes(){
//ignorar solicitação se uma requisição já estiver em curso
if (tableStateNotifier.value['status'] == TableStatus.loading) return;
if (tableStateNotifier.value['itemType'] != ItemType.nation){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': ItemType.nation
};
}
var nationsUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/nation/random_nation',
queryParameters: {'size': '10'});
http.read(nationsUri).then( (jsonString){
var nationsJson = jsonDecode(jsonString);
//se já houver nações no estado da tabela...
if (tableStateNotifier.value['status'] != TableStatus.loading) nationsJson = [...tableStateNotifier.value['dataObjects'], ...nationsJson];
tableStateNotifier.value = {
'itemType': ItemType.nation,
'status': TableStatus.ready,
'dataObjects': nationsJson,
'propertyNames': ["nationality","capital","language","national_sport"],
'columnNames': ["Nome", "Capital", "Idioma","Esporte"]
};
});
}
void carregarCervejas(){
//ignorar solicitação se uma requisição já estiver em curso
if (tableStateNotifier.value['status'] == TableStatus.loading) return;
//emitir estado loading se items em exibição não forem cervejas
if (tableStateNotifier.value['itemType'] != ItemType.beer){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': ItemType.beer
};
}
var beersUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/beer/random_beer',
queryParameters: {'size': '10'});
http.read(beersUri).then( (jsonString){
var beersJson = jsonDecode(jsonString);
//se já houver cervejas no estado da tabela...
if (tableStateNotifier.value['status'] != TableStatus.loading) beersJson = [...tableStateNotifier.value['dataObjects'], ...beersJson];
tableStateNotifier.value = {
'itemType': ItemType.beer,
'status': TableStatus.ready,
'dataObjects': beersJson,
'propertyNames': ["name","style","ibu"],
'columnNames': ["Nome", "Estilo", "IBU"]
};
});
}
}
final dataService = DataService();
Parabéns, você acabou de criar uma biblioteca em dart. Vamos criar outra.
Retire os widgets MyApp, DataTableWidget e NewNavBar do main.dart e ponha-os no view/widgets.dart. O arquivo widgets.dart fica assim:
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
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, __){
switch (value['status']){
case TableStatus.idle:
return Center(child: Text("Toque em algum botão"));
case TableStatus.loading:
return Center(child: CircularProgressIndicator());
case TableStatus.ready:
return SingleChildScrollView(child: DataTableWidget(
jsonObjects: value['dataObjects'],
propertyNames: value['propertyNames'],
columnNames: value['columnNames']
)) ;
case TableStatus.error:
return Text("Lascou");
}
return Text("...");
}
),
bottomNavigationBar: NewNavBar(itemSelectedCallback: dataService.carregar),
));
}
}
class NewNavBar extends HookWidget {
final _itemSelectedCallback;
NewNavBar({itemSelectedCallback}):
_itemSelectedCallback = itemSelectedCallback ?? (int){}
@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))
]);
}
}
class DataTableWidget extends StatelessWidget {
final List jsonObjects;
final List<String> columnNames;
final List<String> propertyNames;
DataTableWidget( {this.jsonObjects = const [], this.columnNames = const [], this.propertyNames= const []});
@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());
}
}
- Opa, temos um problema! Erro de compilação... tá dando um Undefined name 'dataService' e mais uma ruma de erro...
Ok. Isso acontece porque os widgets da gente precisam da constante dataService e da enumeração TableStatus para funcionar. Em programação, a gente diz que nossos widgets são dependentes de elementos (enumerações, classes ou variáveis/constantes) que estão na biblioteca data/data_service.dart.
A gente resolve a parada simplesmente importando a biblioteca data/data_service.dart lá em view/widgets.dart.
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import '../data/data_service.dart';
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, __){
switch (value['status']){
case TableStatus.idle:
return Center(child: Text("Toque em algum botão"));
case TableStatus.loading:
return Center(child: CircularProgressIndicator());
case TableStatus.ready:
return SingleChildScrollView(child: DataTableWidget(
jsonObjects: value['dataObjects'],
propertyNames: value['propertyNames'],
columnNames: value['columnNames']
)) ;
case TableStatus.error:
return Text("Lascou");
}
return Text("...");
}
),
bottomNavigationBar: NewNavBar(itemSelectedCallback: dataService.carregar),
));
}
}
class NewNavBar extends HookWidget {
final _itemSelectedCallback;
NewNavBar({itemSelectedCallback}):
_itemSelectedCallback = itemSelectedCallback ?? (int){}
@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))
]);
}
}
class DataTableWidget extends StatelessWidget {
final List jsonObjects;
final List<String> columnNames;
final List<String> propertyNames;
DataTableWidget( {this.jsonObjects = const [], this.columnNames = const [], this.propertyNames= const []});
@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());
}
}
- Beleza, Professor. Só uma coisa: a gente nem teve esse problema de ter que importar código que a gente escreveu quando fizemos o data_service.dart...
Excelente ponto.
É que o código que consta no data_service.dart são classes, enumerações e variáveis de negócio, e esse tipo de código não deve conhecer jamais nada que não seja, também, do negócio. Nada ali deve referenciar, nunca, nenhuma classe de interface gráfica. Do jeito que a gente escreveu o data_service.dart, podemos trocar a interface gráfica inteira sem mexer em uma única linha de negócio. E, acredite, isso é excelente!
Agora confira como ficou o main.dart.
import 'package:flutter/material.dart';
import 'view/widgets.dart';
void main() {
MyApp app = MyApp();
runApp(app);
}
- Vige, só isso?
Só. Programa principal é pra ser pouca coisa mesmo.
Agora que o código tá mais organizado estruturalmente, vamos começar a mexer nele pela parte mais fácil: tem um número mágico nos métodos de carregar... E se você não sabe o que é número mágico, crie vergonha e faça as outras receitas.
(...)
var coffeesUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/coffee/random_coffee',
queryParameters: {'size': '10'});
(...)
Bem, é um "String mágico", na verdade, mas dá no mesmo. E ainda tem em mais dois lugares no código, não vou nem me dar ao trabalho de mostrá-los porque um já é grave o suficiente.
- Porque só um istringuezinho desse é tão grave assim, Professor?
Porque isso limita bastante o funcionamento de um objeto da classe DataService: ele sempre só carrega de 10 em 10 itens. Se a gente quiser diferente, "basta" modificar o código, compilar novamente, fazer o build do app e executá-lo. Ou seja, seboseira grande.
- E o ideal?
Por enquanto, seria criar uma propriedade na classe DataService que represente a quantidade de itens que devem ser buscados por vez e usar essa propriedade - uma variável, no fim das contas - na consulta em vez do número fixo 10. A gente pode até dar o valor 10 como default para essa propriedade.
Vamos fazer isso. Modifique o arquivo data_service.dart.
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
enum TableStatus{idle,loading,ready,error}
enum ItemType{beer, coffee, nation, none}
class DataService{
int numberOfItems = 10;
ValueNotifier<Map<String,dynamic>> tableStateNotifier
= ValueNotifier({
'status':TableStatus.idle,
'dataObjects':[],
'itemType': ItemType.none
});
void carregar(index){
final funcoes = [carregarCafes, carregarCervejas, carregarNacoes];
funcoes[index]();
}
void carregarCafes(){
//ignorar solicitação se uma requisição já estiver em curso
if (tableStateNotifier.value['status'] == TableStatus.loading) return;
if (tableStateNotifier.value['itemType'] != ItemType.coffee){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': ItemType.coffee
};
}
var coffeesUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/coffee/random_coffee',
queryParameters: {'size': '$numberOfItems'});
http.read(coffeesUri).then( (jsonString){
var coffeesJson = jsonDecode(jsonString);
//se já houver cafés no estado da tabela...
if (tableStateNotifier.value['status'] != TableStatus.loading) coffeesJson = [...tableStateNotifier.value['dataObjects'], ...coffeesJson];
tableStateNotifier.value = {
'itemType': ItemType.coffee,
'status': TableStatus.ready,
'dataObjects': coffeesJson,
'propertyNames': ["blend_name","origin","variety"],
'columnNames': ["Nome", "Origem", "Tipo"]
};
});
}
void carregarNacoes(){
//ignorar solicitação se uma requisição já estiver em curso
if (tableStateNotifier.value['status'] == TableStatus.loading) return;
if (tableStateNotifier.value['itemType'] != ItemType.nation){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': ItemType.nation
};
}
var nationsUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/nation/random_nation',
queryParameters: {'size': '$numberOfItems'});
http.read(nationsUri).then( (jsonString){
var nationsJson = jsonDecode(jsonString);
//se já houver nações no estado da tabela...
if (tableStateNotifier.value['status'] != TableStatus.loading) nationsJson = [...tableStateNotifier.value['dataObjects'], ...nationsJson];
tableStateNotifier.value = {
'itemType': ItemType.nation,
'status': TableStatus.ready,
'dataObjects': nationsJson,
'propertyNames': ["nationality","capital","language","national_sport"],
'columnNames': ["Nome", "Capital", "Idioma","Esporte"]
};
});
}
void carregarCervejas(){
//ignorar solicitação se uma requisição já estiver em curso
if (tableStateNotifier.value['status'] == TableStatus.loading) return;
//emitir estado loading se items em exibição não forem cervejas
if (tableStateNotifier.value['itemType'] != ItemType.beer){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': ItemType.beer
};
}
var beersUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/beer/random_beer',
queryParameters: {'size': '$numberOfItems'});
http.read(beersUri).then( (jsonString){
var beersJson = jsonDecode(jsonString);
//se já houver cervejas no estado da tabela...
if (tableStateNotifier.value['status'] != TableStatus.loading) beersJson = [...tableStateNotifier.value['dataObjects'], ...beersJson];
tableStateNotifier.value = {
'itemType': ItemType.beer,
'status': TableStatus.ready,
'dataObjects': beersJson,
'propertyNames': ["name","style","ibu"],
'columnNames': ["Nome", "Estilo", "IBU"]
};
});
}
}
final dataService = DataService();
- Pra mim, isso dá no mesmo...
Dá no mesmo, mas não é o mesmo. Vamos ver uma coisa que a gente pode fazer e que não poderíamos na versão anterior, vamos implementar essa funcionalidade aqui:
Modifique a classe MyApp, acrescentando o menu no AppBar.
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"),
actions:[
PopupMenuButton(
itemBuilder: (_) => [3,7,15].map(
(num) => PopupMenuItem(
value: num,
child: Text("Carregar $num itens por vez"),
)
).toList(),
onSelected: (number){
dataService.numberOfItems = number;
},
)
]
),
body: ValueListenableBuilder(
valueListenable: dataService.tableStateNotifier,
builder:(_, value, __){
switch (value['status']){
case TableStatus.idle:
return Center(child: Text("Toque em algum botão"));
case TableStatus.loading:
return Center(child: CircularProgressIndicator());
case TableStatus.ready:
return SingleChildScrollView(child: DataTableWidget(
jsonObjects: value['dataObjects'],
propertyNames: value['propertyNames'],
columnNames: value['columnNames']
)) ;
case TableStatus.error:
return Text("Lascou");
}
return Text("...");
}
),
bottomNavigationBar: NewNavBar(itemSelectedCallback: dataService.carregar),
));
}
}
Bem, a gente cria um menu com alguns itens, que é basicamente saber que classe modela o que e chamar os construtores para criar os objetos corretamente. No caso, criamos um objeto PopupMenu para representar o, bem, menu de popup, aquele com os 3 pontinhos (é o ícone default, não precisamos sequer especificá-lo)...
(...)
PopupMenuButton(
itemBuilder: (_) => [3,7,15].map(
(num) => PopupMenuItem(
value: num,
child: Text("Carregar $num itens por vez"),
)
).toList(),
onSelected: (number){
dataService.numberOfItems = number;
},
)
(...)
E para a fábrica do PopupMenu passamos dois atributos nomeados.
O itemBuilder é uma função que recebe um parâmetro que não nos interessa e retorna uma lista de 3 objetos PopupMenuItem...
(...)
PopupMenuButton(
itemBuilder: (_) => [3,7,15].map(
(num) => PopupMenuItem(
value: num,
child: Text("Carregar $num itens por vez"),
)
).toList(),
onSelected: (number){
dataService.numberOfItems = number;
},
)
(...)
E o onSelected é uma função que é chamada quando um PopupMenuItem é selecionado, passando-se para essa o value associado ao PopupMenuItem (3, 7 ou 15, no caso). Simplesmente estamos pegando esse valor que é passado e empurrando na propriedade numberOfItens do objeto dataService. Os seja, podemos mudar a quantidade de itens buscados em tempo de execução, com aquela simples alteração que fizemos no código da classe DataService.
(...)
PopupMenuButton(
itemBuilder: (_) => [3,7,15].map(
(num) => PopupMenuItem(
value: num,
child: Text("Carregar $num itens por vez"),
)
).toList(),
onSelected: (number){
dataService.numberOfItems = number;
},
)
(...)
- Já sei, vei ter mais coisa, porque sempre tem.
Sempre tem.
O problema com esse código agora é o DataService, é que ele está digamos, "desprotegido".
Aquela propriedade numberOfItens é pública, o que significa que qualquer objeto de seu app pode mexer nela do jeito que bem entender.
Então meio que se um estagiário como você, daqueles que se apertarem x na barra de endereços do navegador ele vai completar com videos.com logo de primeira, se um estagiário desse mexer na propriedade de forma errada, isso pode acanalhar seu objeto DataService e, consequentemente, seu app. Por exemplo, num onSelected desses nada impede do cara fazer isso:
(...)
PopupMenuButton(
itemBuilder: (_) => [3,7,15].map(
(num) => PopupMenuItem(
value: num,
child: Text("Carregar $num itens por vez"),
)
).toList(),
onSelected: (number){
dataService.numberOfItems = -10;
},
)
(...)
Nesse caso todas as suas requisições subsequentes iriam falhar porque a API provavelmente não iria aceitar um valor negativo. O cara poderia por um valor arbitrariamente grande também, e a API falharia. Isso acontece porque a propriedade é pública, todo mundo pode fazer o que quiser com ela, e essa característica está trazendo certa fragilidade para a classe DataService.
Em inglês, a gente diz que essa classe não é fool proof - não é à prova de caba besta, traduzindo livremente.
- E tem como ser à prova de caba besta, Professor?
Tem sim, com um assunto novo, mas nem tanto.
A orientação a objetos introduziu restrições à visibilidade das propriedades dos objetos das classes. A gente chamava isso de information hiding, lá quando eu tinha sua idade, não muito tempo atrás. A gente pode esconder aquela propriedade numberOfItens do resto do mundo de uma forma bem simples:
Modifique a classe DataService.
class DataService{
int _numberOfItems = 10;
ValueNotifier<Map<String,dynamic>> tableStateNotifier
= ValueNotifier({
'status':TableStatus.idle,
'dataObjects':[],
'itemType': ItemType.none
});
void carregar(index){
final funcoes = [carregarCafes, carregarCervejas, carregarNacoes];
funcoes[index]();
}
void carregarCafes(){
//ignorar solicitação se uma requisição já estiver em curso
if (tableStateNotifier.value['status'] == TableStatus.loading) return;
if (tableStateNotifier.value['itemType'] != ItemType.coffee){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': ItemType.coffee
};
}
var coffeesUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/coffee/random_coffee',
queryParameters: {'size': '$_numberOfItems'});
http.read(coffeesUri).then( (jsonString){
var coffeesJson = jsonDecode(jsonString);
//se já houver cafés no estado da tabela...
if (tableStateNotifier.value['status'] != TableStatus.loading) coffeesJson = [...tableStateNotifier.value['dataObjects'], ...coffeesJson];
tableStateNotifier.value = {
'itemType': ItemType.coffee,
'status': TableStatus.ready,
'dataObjects': coffeesJson,
'propertyNames': ["blend_name","origin","variety"],
'columnNames': ["Nome", "Origem", "Tipo"]
};
});
}
void carregarNacoes(){
//ignorar solicitação se uma requisição já estiver em curso
if (tableStateNotifier.value['status'] == TableStatus.loading) return;
if (tableStateNotifier.value['itemType'] != ItemType.nation){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': ItemType.nation
};
}
var nationsUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/nation/random_nation',
queryParameters: {'size': '$_numberOfItems'});
http.read(nationsUri).then( (jsonString){
var nationsJson = jsonDecode(jsonString);
//se já houver nações no estado da tabela...
if (tableStateNotifier.value['status'] != TableStatus.loading) nationsJson = [...tableStateNotifier.value['dataObjects'], ...nationsJson];
tableStateNotifier.value = {
'itemType': ItemType.nation,
'status': TableStatus.ready,
'dataObjects': nationsJson,
'propertyNames': ["nationality","capital","language","national_sport"],
'columnNames': ["Nome", "Capital", "Idioma","Esporte"]
};
});
}
void carregarCervejas(){
//ignorar solicitação se uma requisição já estiver em curso
if (tableStateNotifier.value['status'] == TableStatus.loading) return;
//emitir estado loading se items em exibição não forem cervejas
if (tableStateNotifier.value['itemType'] != ItemType.beer){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': ItemType.beer
};
}
var beersUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/beer/random_beer',
queryParameters: {'size': '$_numberOfItems'});
http.read(beersUri).then( (jsonString){
var beersJson = jsonDecode(jsonString);
//se já houver cervejas no estado da tabela...
if (tableStateNotifier.value['status'] != TableStatus.loading) beersJson = [...tableStateNotifier.value['dataObjects'], ...beersJson];
tableStateNotifier.value = {
'itemType': ItemType.beer,
'status': TableStatus.ready,
'dataObjects': beersJson,
'propertyNames': ["name","style","ibu"],
'columnNames': ["Nome", "Estilo", "IBU"]
};
});
}
}
A gente apenas pôs um underline antes do nome da propriedade. Isso faz com que a visibilidade da propriedade _numberOfItens seja private (privada).
- Como assim, privada? Privada em relação a quem, Professor?
Privada em relação a qualquer código que esteja na mesma biblioteca onde a "propriedade privada" é declarada. Só quem mora naquele sítio pode chupar aquela laranja. Nesse caso, só tem acesso a dataService._numberOfItens algum código que esteja escrito no diretório data. Porque todo diretório dart é uma biblioteca, lembra? Então o diretório define o escopo de visibilidade das variáveis declaradas com underline. Não há modificadores específicos, como os private, protected e public de Java/C++. Se você tasca o underline, é visível apenas no diretório. Não não tasca, é visível pra todo mundo. No dart, ou é fazenda, ou é praça, ainda que algumas bibliotecas tentem criar uma visibilidade intermediária (não vamos nem precisamos entrar nisso).
- Ah, professor, mas aí deu bode. Lá no onSelected, que é fora da biblioteca data, a gente precisa modificar essa propriedade numberOfItens, né não?
Né sim. E se a gente tentar vai dar erro.
Modifique a classe MyApp...
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"),
actions:[
PopupMenuButton(
itemBuilder: (_) => [3,7,15].map(
(num) => PopupMenuItem(
value: num,
child: Text("Carregar $num itens por vez"),
)
).toList(),
onSelected: (number){
dataService._numberOfItems = number;
},
)
]
),
body: ValueListenableBuilder(
valueListenable: dataService.tableStateNotifier,
builder:(_, value, __){
switch (value['status']){
case TableStatus.idle:
return Center(child: Text("Toque em algum botão"));
case TableStatus.loading:
return Center(child: CircularProgressIndicator());
case TableStatus.ready:
return SingleChildScrollView(child: DataTableWidget(
jsonObjects: value['dataObjects'],
propertyNames: value['propertyNames'],
columnNames: value['columnNames']
)) ;
case TableStatus.error:
return Text("Lascou");
}
return Text("...");
}
),
bottomNavigationBar: NewNavBar(itemSelectedCallback: dataService.carregar),
));
}
}
O compilador deve estar reclamando da linha em destaque. Não é permitida a alteração daquela propriedade privada de fora da fazenda do dataService - e a classe MayApp está fora dela, definitivamente, pois está na "fazenda view".
O próprio compilador dá a dica de como resolver a parada pra gente, ele indica a criação de um método setter. Métodos setter são funções cujo objetivo é receber alguma informação como parâmetro e guardar essa informação numa propriedade com visibilidade privada. Um método set não poderia ser mais simples de se escrever.
Modifique a classe DataService...
class DataService{
int _numberOfItems = 10;
set numberOfItens(n){
_numberOfItems = n;
}
final ValueNotifier<Map<String,dynamic>> tableStateNotifier
= ValueNotifier({
'status':TableStatus.idle,
'dataObjects':[],
'itemType': ItemType.none
});
void carregar(index){
final funcoes = [carregarCafes, carregarCervejas, carregarNacoes];
funcoes[index]();
}
void carregarCafes(){
//ignorar solicitação se uma requisição já estiver em curso
if (tableStateNotifier.value['status'] == TableStatus.loading) return;
if (tableStateNotifier.value['itemType'] != ItemType.coffee){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': ItemType.coffee
};
}
var coffeesUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/coffee/random_coffee',
queryParameters: {'size': '$_numberOfItems'});
http.read(coffeesUri).then( (jsonString){
var coffeesJson = jsonDecode(jsonString);
//se já houver cafés no estado da tabela...
if (tableStateNotifier.value['status'] != TableStatus.loading) coffeesJson = [...tableStateNotifier.value['dataObjects'], ...coffeesJson];
tableStateNotifier.value = {
'itemType': ItemType.coffee,
'status': TableStatus.ready,
'dataObjects': coffeesJson,
'propertyNames': ["blend_name","origin","variety"],
'columnNames': ["Nome", "Origem", "Tipo"]
};
});
}
void carregarNacoes(){
//ignorar solicitação se uma requisição já estiver em curso
if (tableStateNotifier.value['status'] == TableStatus.loading) return;
if (tableStateNotifier.value['itemType'] != ItemType.nation){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': ItemType.nation
};
}
var nationsUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/nation/random_nation',
queryParameters: {'size': '$_numberOfItems'});
http.read(nationsUri).then( (jsonString){
var nationsJson = jsonDecode(jsonString);
//se já houver nações no estado da tabela...
if (tableStateNotifier.value['status'] != TableStatus.loading) nationsJson = [...tableStateNotifier.value['dataObjects'], ...nationsJson];
tableStateNotifier.value = {
'itemType': ItemType.nation,
'status': TableStatus.ready,
'dataObjects': nationsJson,
'propertyNames': ["nationality","capital","language","national_sport"],
'columnNames': ["Nome", "Capital", "Idioma","Esporte"]
};
});
}
void carregarCervejas(){
//ignorar solicitação se uma requisição já estiver em curso
if (tableStateNotifier.value['status'] == TableStatus.loading) return;
//emitir estado loading se items em exibição não forem cervejas
if (tableStateNotifier.value['itemType'] != ItemType.beer){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': ItemType.beer
};
}
var beersUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/beer/random_beer',
queryParameters: {'size': '$_numberOfItems'});
http.read(beersUri).then( (jsonString){
var beersJson = jsonDecode(jsonString);
//se já houver cervejas no estado da tabela...
if (tableStateNotifier.value['status'] != TableStatus.loading) beersJson = [...tableStateNotifier.value['dataObjects'], ...beersJson];
tableStateNotifier.value = {
'itemType': ItemType.beer,
'status': TableStatus.ready,
'dataObjects': beersJson,
'propertyNames': ["name","style","ibu"],
'columnNames': ["Nome", "Estilo", "IBU"]
};
});
}
}
A gente definiu um método que recebe um inteiro e põe esse parâmetro na propriedade privada _numberOfItens. A sacada é que esse método é publico, qualquer um pode chamá-lo. E, por ser um método set, a gente chama de um jeito diferente, sem usar parênteses e como se fosse uma atribuição normal de variáveis.
Altere a classe MyApp...
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"),
actions:[
PopupMenuButton(
itemBuilder: (_) => [3,7,15].map(
(num) => PopupMenuItem(
value: num,
child: Text("Carregar $num itens por vez"),
)
).toList(),
onSelected: (number){
dataService.numberOfItems = number;
},
)
]
),
body: ValueListenableBuilder(
valueListenable: dataService.tableStateNotifier,
builder:(_, value, __){
switch (value['status']){
case TableStatus.idle:
return Center(child: Text("Toque em algum botão"));
case TableStatus.loading:
return Center(child: CircularProgressIndicator());
case TableStatus.ready:
return SingleChildScrollView(child: DataTableWidget(
jsonObjects: value['dataObjects'],
propertyNames: value['propertyNames'],
columnNames: value['columnNames']
)) ;
case TableStatus.error:
return Text("Lascou");
}
return Text("...");
}
),
bottomNavigationBar: NewNavBar(itemSelectedCallback: dataService.carregar),
));
}
}
Note, que se fosse um "método normal", a gente chamaria dataService.numberOfItens(number), mas como é um método get a gente chama dataService.numberOfItems = number;
Note que não deixa de ser uma chamada de método. Essa notação usada, no entanto, melhora a legibilidade do código porque a gente sabe que, por mais que um método esteja sendo invocado, o objetivo é o de fazer uma atribuição a uma das propriedades do objeto sobre o qual o método é invocado.
- Muito bonito, Professor. Mas agora aquele estagiário que vive vendo filme de putaria e que não tem nada a ver comigo pode, novamente, botar qualquer valor lá na propriedade. Basta chamar dataService.numberOfItems = -799; que acanalha do mesmo jeito que acanalhava quando a propriedade era pública.
Excelente observação. Programadores OO das antigas relutam em admitir isso, mas se for pra escrever métodos set como esse que a gente escreveu, é melhor deixar a visibilidade como pública. No entanto, já que a única via de acesso ao _numberOfItems é pelo método set, a gente pode implementar esse método de forma diferente, deixando nossa propriedade à prova de caba besta.
set numberOfItems(n){
_numberOfItems = n <= 0 ? 3: n > 15? 15: n;
}
Pronto, agora se o estagiário fizer dataService.numberOfItems = -987, a propriedade vai ficar com valor 3. Se fizer dataService.numberOfItems = 379, a propriedade vai ficar com valor 15. E se atribuir qualquer número entre 1 e 15, a operação se dará normalmente. Em resumo, nosso objeto DataService não estará mais desprotegido nesse sentido.
- Mas que esses 3 e 15 tão como cara de números mágicos, tão...
Tão e são. E aquele 10 também é.
Modifique a classe DataService.
class DataService{
static const MAX_N_ITEMS = 15;
static const MIN_N_ITEMS = 3;
static const DEFAULT_N_ITEMS = 7;
int _numberOfItems = DEFAULT_N_ITEMS;
set numberOfItems(n){
_numberOfItems = n < 0 ? MIN_N_ITEMS: n > MAX_N_ITEMS? MAX_N_ITEMS: n;
}
final ValueNotifier<Map<String,dynamic>> tableStateNotifier
= ValueNotifier({
'status':TableStatus.idle,
'dataObjects':[],
'itemType': ItemType.none
});
void carregar(index){
final funcoes = [carregarCafes, carregarCervejas, carregarNacoes];
funcoes[index]();
}
void carregarCafes(){
//ignorar solicitação se uma requisição já estiver em curso
if (tableStateNotifier.value['status'] == TableStatus.loading) return;
if (tableStateNotifier.value['itemType'] != ItemType.coffee){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': ItemType.coffee
};
}
var coffeesUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/coffee/random_coffee',
queryParameters: {'size': '$_numberOfItems'});
http.read(coffeesUri).then( (jsonString){
var coffeesJson = jsonDecode(jsonString);
//se já houver cafés no estado da tabela...
if (tableStateNotifier.value['status'] != TableStatus.loading) coffeesJson = [...tableStateNotifier.value['dataObjects'], ...coffeesJson];
tableStateNotifier.value = {
'itemType': ItemType.coffee,
'status': TableStatus.ready,
'dataObjects': coffeesJson,
'propertyNames': ["blend_name","origin","variety"],
'columnNames': ["Nome", "Origem", "Tipo"]
};
});
}
void carregarNacoes(){
//ignorar solicitação se uma requisição já estiver em curso
if (tableStateNotifier.value['status'] == TableStatus.loading) return;
if (tableStateNotifier.value['itemType'] != ItemType.nation){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': ItemType.nation
};
}
var nationsUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/nation/random_nation',
queryParameters: {'size': '$_numberOfItems'});
http.read(nationsUri).then( (jsonString){
var nationsJson = jsonDecode(jsonString);
//se já houver nações no estado da tabela...
if (tableStateNotifier.value['status'] != TableStatus.loading) nationsJson = [...tableStateNotifier.value['dataObjects'], ...nationsJson];
tableStateNotifier.value = {
'itemType': ItemType.nation,
'status': TableStatus.ready,
'dataObjects': nationsJson,
'propertyNames': ["nationality","capital","language","national_sport"],
'columnNames': ["Nome", "Capital", "Idioma","Esporte"]
};
});
}
void carregarCervejas(){
//ignorar solicitação se uma requisição já estiver em curso
if (tableStateNotifier.value['status'] == TableStatus.loading) return;
//emitir estado loading se items em exibição não forem cervejas
if (tableStateNotifier.value['itemType'] != ItemType.beer){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': ItemType.beer
};
}
var beersUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/beer/random_beer',
queryParameters: {'size': '$_numberOfItems'});
http.read(beersUri).then( (jsonString){
var beersJson = jsonDecode(jsonString);
//se já houver cervejas no estado da tabela...
if (tableStateNotifier.value['status'] != TableStatus.loading) beersJson = [...tableStateNotifier.value['dataObjects'], ...beersJson];
tableStateNotifier.value = {
'itemType': ItemType.beer,
'status': TableStatus.ready,
'dataObjects': beersJson,
'propertyNames': ["name","style","ibu"],
'columnNames': ["Nome", "Estilo", "IBU"]
};
});
}
}
Pronto. Pode salvar e rodar que a mágica acontece.
De quebra você aprendeu a escrever constantes simbólicas - é isso que são aquelas 3 que definimos lá no início da classe. Constantes simbólicas servem, entre outras coisas, para limpar números mágicos do código e torná-lo mais legível. Devem ser escritas em letras capitulares e nomes compostos devem ser separados por underline.
Eu tenho consciência de que quem lê esses textos adora ver alguma coisa azul para ter algo prático a cumprir, mas a gente precisa de 1 real e 99 de debate um tanto teórico nesse tema.
Primeiramente, é comum que se confunda esse conceito de information hiding ou restrição de visibilidade com o conceito de encapsulamento. Há livros e livros e páginas e vídeos que dão uma coisa pela outra, quando, para mim, são coisas bem distintas.
Encapsulamento é empurrar duas coisas numa mesma criatura, que é a classe: essas duas coisas são informação e comportamento. Que são traduzidos em código na forma de propriedades (ou atributos) e métodos (funções). Não parece nada tão revolucionário mas faz uma diferença da porra ter numa mesma entidade variáveis e funções que operam sobre elas, porque a gente meio que é forçado a fazer unidades coesas e pequenas de software - uma vez mais, as classes.
Antes da OO, a gente agrupava informações, e só elas, em registros, que eram meio que classes só com variáveis dentro e sem funções - e não era senão com uma gambiarra muito grande que a gente poderia por uma função num registro (em C, a gente chama estrutura). Assim as bibliotecas tinham variáveis soltas, tipos agregadores de informação como os registros e funções igualmente soltas que não tinham nenhum compromisso de operar sobre aquelas variáveis ou aqueles tipos que estavam na mesma biblioteca que elas. A possibilidade de algum bebedor de caipirinha de vitamina de banana com leite condensado fazer besteira era muito grande. Ainda assim, claro, muito software de qualidade foi produzido naqueles tempos em que programação era bam mais arte que ciência, assim como também dá pra fazer besteira sob os domínios da OO.
Nesse sentido, encapsulamento não tem nada a ver com visibilidade de atributos. Quando a gente escreveu a classe DataService e botou lá um atributo tableStateNotifier e outro atributo numberOfItems e mais uma ruma de função carregar*() que, de maneira coesa, operam sobre aquelas informações representadas nos atributos, a gente tava fazendo encampusamento. O information hiding apareceu apenas quando a gente meteu o underline e mudou um atributo para _numberOfItems. Mas como eu sou apenas um Zé Mané liso e professor de OO numa universidade interiorana, vou deixar esse ponto como uma opinião minha e não como um fato. Eu não estou sozinho nesse ponto de vista, claro, como vocês podem ver mais adiante no material complementar.
há um método complementar ao método set, que é o método get. A função dos métodos get é a de dar acesso de leitura (o set é para modificação) a uma propriedade privada. Pesquise por dart getter methods e escreva um método get para a propriedade _numberOfItems (nenhuma funcionalidade, ainda, usa esse método, apenas escreva-o e deixe-o lá.
retire duplicação de código nas funções carregar. Já pedi isso em outras ocasiões.
fazer com que o item de menu fique selecionado. Talvez você precise daqueeele método get aqui, mas não apenas disso.
as quantidades 3, 5 e 7 são, em si, números mágicos. Essa lista deve estar originalmente em outro lugar e não hardcoded ali onde está. Por enquanto, é a interface gráfica quem tem a inteligência de saber quantos menus devem ser montados e com que valores. Será que é a classe MyApp que tem que decidir isso? Ajeite seu código.
A tabela não tem scroll horizontal. Ajeite isso.