Objeto: criação de um pequeno app com ordenação de tabela por colunas.
Principais termos técnicos abordados: polimorfismo, 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
Eu vou ter que fazer uma coisa que odeio e que você não deve fazer em hipótese alguma: implementar um código genérico que já está implementado em alguma biblioteca.
Desculpe, crianças, mas a gente vai fingir que não existe método de ordenação disponível e a gente vai implementar o nosso próprio método de ordenação e a gente só vai fazer isso porque não consegui pensar em nenhum exemplo mais didático que coubesse no app que a gente vem desenvolvendo.
Basicamente, a gente vai partir do código implementado na receita anterior e vai acrescentar a funcionalidade de ordenar a tabela que exibe os dados, por diversas colunas. Nosso objetivo é ter um ordenador que ordene qualquer coisa, mas não vamos atingir esse objetivo nesse momento.
Porque essa receita vai motivar o uso de polimorfismo e tipos abstratos, e precisarei que você implemente errado para na próxima poder implementar certo e entender como os caras que realmente sabem programar fazem as coisas.
Mais adiante é que resolveremos com polimorfismo os problemas que forçarei você a ter aqui.
Abra o código resultante da receita anterior. Se não a fez, faça.
A gente tem a possibilidade de fazer a ordenação no DataService e modificar o estado com a nova lista ordenada. Nesse caso o DataTableWidget seria redesenhado automaticamente, pois está debaixo de um ValueListenableBuilder conectado com o estado do DataTableWidget.
É o caminho mais fácil.
Crie um diretório util, detro do diretório lib e, dentro de util, crie um arquivo ordenador.dart com o seguinte código:
class Ordenador{
List ordenarCervejasPorNomeCrescente(List cervejas){
List cervejasOrdenadas = List.of(cervejas);
bool trocouAoMenosUm;
do{
trocouAoMenosUm = false;
for (int i=0; i<cervejasOrdenadas.length-1; i++){
var atual = cervejasOrdenadas[i];
var proximo = cervejasOrdenadas[i+1];
if (atual["name"].compareTo(proximo["name"]) > 0){
var aux = cervejasOrdenadas[i];
cervejasOrdenadas[i] = cervejasOrdenadas[i+1];
cervejasOrdenadas[i+1] = aux;
trocouAoMenosUm = true;
}
}
}while(trocouAoMenosUm);
return cervejasOrdenadas;
}
}
Essa é uma versão aportuguesada do algoritmo do bubble sort, que é provavelmente o método de ordenação mais xibata que existe, mas isso não importa aqui. O que importa é que esse método ordena uma lista de objetos JSON pela propriedade name, sem modificar a lista original.
Vamos modificar o DataService da receita anterior e acrescentar um método de ordenação.
Antes disso, subistitua o DataService corrente por uma versão mais bem pragramada e dê uma olhada nela:
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;
String get asString => '$name';
List<String> get columns => this == coffee? ["Nome", "Origem", "Tipo"] :
this == beer? ["Nome", "Estilo", "IBU"]:
this == nation? ["Nome", "Capital", "Idioma","Esporte"]:
[] ;
List<String> get properties => this == coffee? ["blend_name","origin","variety"] :
this == beer? ["name","style","ibu"]:
this == nation? ["nationality","capital","language","national_sport"]:
[] ;
}
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 params = [ItemType.coffee, ItemType.beer, ItemType.nation];
carregarPorTipo(params[index]);
}
Uri montarUri(ItemType type){
return Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/${type.asString}/random_${type.asString}',
queryParameters: {'size': '$_numberOfItems'});
}
Future<List<dynamic>> acessarApi(Uri uri) async{
var jsonString = await http.read(uri);
var json = jsonDecode(jsonString);
json = [...tableStateNotifier.value['dataObjects'], ...json];
return json;
}
void emitirEstadoCarregando(ItemType type){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': type
};
}
void emitirEstadoPronto(ItemType type, var json){
tableStateNotifier.value = {
'itemType': type,
'status': TableStatus.ready,
'dataObjects': json,
'propertyNames': type.properties,
'columnNames': type.columns
};
}
bool temRequisicaoEmCurso() => tableStateNotifier.value['status'] == TableStatus.loading;
bool mudouTipoDeItemRequisitado(ItemType type) => tableStateNotifier.value['itemType'] != type;
void carregarPorTipo(ItemType type) async{
//ignorar solicitação se uma requisição já estiver em curso
if (temRequisicaoEmCurso()) return;
if (mudouTipoDeItemRequisitado(type)){
emitirEstadoCarregando(type);
}
var uri = montarUri(type);
var json = await acessarApi(uri);
emitirEstadoPronto(type, json);
}
}
final dataService = DataService();
Essa versão tem menos de 100 linhas e está bem mais modularizada, bem mais legível. Então vá lendo com calma e vá aprendendo a fazer as coisas direito.
Bem, nesse DataService mais bonitinho, a gente precisa de um método para ordenar. Esse método deve, claro, chamar o nosso ordenador lá na biblioteca util. Vou por esse método para receber a propriedade de ordenação. Por enquanto, ordenamos apenas por nome, apenas lista de cervejas e apenas em ordem crescente.
Estamos começando, apenas.
Edite o DataService.
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../util/ordenador.dart';
enum TableStatus{idle,loading,ready,error}
enum ItemType{
beer, coffee, nation, none;
String get asString => '$name';
List<String> get columns => this == coffee? ["Nome", "Origem", "Tipo"] :
this == beer? ["Nome", "Estilo", "IBU"]:
this == nation? ["Nome", "Capital", "Idioma","Esporte"]:
[] ;
List<String> get properties => this == coffee? ["blend_name","origin","variety"] :
this == beer? ["name","style","ibu"]:
this == nation? ["nationality","capital","language","national_sport"]:
[] ;
}
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 params = [ItemType.coffee, ItemType.beer, ItemType.nation];
carregarPorTipo(params[index]);
}
void ordenarEstadoAtual(String propriedade){
List objetos = tableStateNotifier.value['dataObjects'] ?? [];
if (objetos == []) return;
Ordenador ord = Ordenador();
var objetosOrdenados = [];
final type = tableStateNotifier.value['itemType'];
if (type == ItemType.beer && propriedade == "name"){
objetosOrdenados = ord.ordenarCervejasPorNomeCrescente(objetos);
}
emitirEstadoOrdenado(objetosOrdenados, propriedade);
}
Uri montarUri(ItemType type){
return Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/${type.asString}/random_${type.asString}',
queryParameters: {'size': '$_numberOfItems'});
}
Future<List<dynamic>> acessarApi(Uri uri) async{
var jsonString = await http.read(uri);
var json = jsonDecode(jsonString);
json = [...tableStateNotifier.value['dataObjects'], ...json];
return json;
}
void emitirEstadoOrdenado(List objetosOrdenados, String propriedade){
var estado = tableStateNotifier.value;
estado['dataObjects'] = objetosOrdenados;
estado['sortCriteria'] = propriedade;
estado['ascending'] = true;
tableStateNotifier.value = estado;
}
void emitirEstadoCarregando(ItemType type){
tableStateNotifier.value = {
'status': TableStatus.loading,
'dataObjects': [],
'itemType': type
};
}
void emitirEstadoPronto(ItemType type, var json){
tableStateNotifier.value = {
'itemType': type,
'status': TableStatus.ready,
'dataObjects': json,
'propertyNames': type.properties,
'columnNames': type.columns
};
}
bool temRequisicaoEmCurso() => tableStateNotifier.value['status'] == TableStatus.loading;
bool mudouTipoDeItemRequisitado(ItemType type) => tableStateNotifier.value['itemType'] != type;
void carregarPorTipo(ItemType type) async{
//ignorar solicitação se uma requisição já estiver em curso
if (temRequisicaoEmCurso()) return;
if (mudouTipoDeItemRequisitado(type)){
emitirEstadoCarregando(type);
}
var uri = montarUri(type);
var json = await acessarApi(uri);//, type);
emitirEstadoPronto(type, json);
}
}
final dataService = DataService();
- Porque não implementamos o algoritmo de ordenação direto no DataService, Professor?
Porque queremos, idealmente, um código genérico, que sirva para qualquer projeto. O DataService é uma classe muito desse app, suas responsabilidades são mais específicas e dizem respeito ao acesso a dados da API.
- E porque tem um if só no método ordenarEstadoAtual?
Porque, por enquanto, vamos ordenar apenas cervejas, por nome e em ordem crescente.
Mas temos grandes planos para ordenar por tudo.
Bem, até agora a gente tem que o nosso DataService é capaz de receber uma chamada para ordenar o estado corrente por uma de suas propriedades. Ao receber essa chamada, o objeto repassa a tarefa de ordenar para um objeto Ordenador e emite um novo estado com a lista ordenada e algumas propriedades descrevendo o tipo de ordenação que foi feita.
Mas a gente precisa chamar esse método a partir de algum clique na interface gráfica, né? Se não, nada acontece.
Vou fazer isso do jeito mais seboso, que sempre é mais fácil, de depois a gente ajeita.
Edite o seu DataTableWidget, lá no arquivo widgets.dart.
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(
onSort: (columnIndex, ascending) =>
dataService.ordenarEstadoAtual(propertyNames[columnIndex]),
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());
}
}
Acrescentamos uma mísera linha (em destaque) que dividimos em duas para ficar mais legível. É uma linha sebosa e eu espero que você entenda o porquê, mas é uma linha simples e efetiva para uma primeira explicação. Nela estamos apenas invocando o método que havíamos implementado no DataService. Acontece que cada objeto DataColumn tem uma função de callback chamada onSort. O objeto DataColumn chama essa função ao clique no cabeçalho da coluna e passa como parêmetro, ora, ora, o índice da coluna e um booleano indicando se a ordem é crescente. Ou seja: o que a gente precisa!!!
Salve o arquivo, rodeo app e veja que a mágica já acontece se você carregar cervejas e tocar na coluna Nome.
Temos, no entanto, algumas deficiências e muitas coisas a fazer.
Primeiramente, nosso ordenador apenas ordena em ordem crescente. E queremos um Ordenador que ordene qualquer lista de objeto, em qualquer ordem e por qualquer critério.
Edite o Ordenador, inicialmente acrescentando métodos para ordenar cervejas por nome e por estilo, em ordem crecente ou decrescente.
class Ordenador{
List ordenarCervejasPorNomeCrescente(List cervejas){
List cervejasOrdenadas = List.of(cervejas);
bool trocouAoMenosUm;
do{
trocouAoMenosUm = false;
for (int i=0; i<cervejasOrdenadas.length-1; i++){
var atual = cervejasOrdenadas[i];
var proximo = cervejasOrdenadas[i+1];
if (atual["name"].compareTo(proximo["name"]) > 0){
var aux = cervejasOrdenadas[i];
cervejasOrdenadas[i] = cervejasOrdenadas[i+1];
cervejasOrdenadas[i+1] = aux;
trocouAoMenosUm = true;
}
}
}while(trocouAoMenosUm);
return cervejasOrdenadas;
}
List ordenarCervejasPorNomeDecrescente(List cervejas){
List cervejasOrdenadas = List.of(cervejas);
bool trocouAoMenosUm;
do{
trocouAoMenosUm = false;
for (int i=0; i<cervejasOrdenadas.length-1; i++){
var atual = cervejasOrdenadas[i];
var proximo = cervejasOrdenadas[i+1];
if (atual["name"].compareTo(proximo["name"]) > 0){
var aux = cervejasOrdenadas[i];
cervejasOrdenadas[i] = cervejasOrdenadas[i+1];
cervejasOrdenadas[i+1] = aux;
trocouAoMenosUm = true;
}
}
}while(trocouAoMenosUm);
return cervejasOrdenadas;
}
List ordenarCervejasPorEstiloCrescente(List cervejas){
List cervejasOrdenadas = List.of(cervejas);
bool trocouAoMenosUm;
do{
trocouAoMenosUm = false;
for (int i=0; i<cervejasOrdenadas.length-1; i++){
var atual = cervejasOrdenadas[i];
var proximo = cervejasOrdenadas[i+1];
if (atual["style"].compareTo(proximo["style"]) > 0){
var aux = cervejasOrdenadas[i];
cervejasOrdenadas[i] = cervejasOrdenadas[i+1];
cervejasOrdenadas[i+1] = aux;
trocouAoMenosUm = true;
}
}
}while(trocouAoMenosUm);
return cervejasOrdenadas;
}
List ordenarCervejasPorEstiloDecrescente(List cervejas){
List cervejasOrdenadas = List.of(cervejas);
bool trocouAoMenosUm;
do{
trocouAoMenosUm = false;
for (int i=0; i<cervejasOrdenadas.length-1; i++){
var atual = cervejasOrdenadas[i];
var proximo = cervejasOrdenadas[i+1];
if (atual["style"].compareTo(proximo["style"]) > 0){
var aux = cervejasOrdenadas[i];
cervejasOrdenadas[i] = cervejasOrdenadas[i+1];
cervejasOrdenadas[i+1] = aux;
trocouAoMenosUm = true;
}
}
}while(trocouAoMenosUm);
return cervejasOrdenadas;
}
}
Aqui já vai dar pra ver que esse tipo de solução vai complicar nossa vida.
A gente tem 4 métodos idênticos, com tendência a muitos e muitos outros escritos da mesma forma.
- E se a gente passar a propriedade como parâmetro pro método de ordenação, Professor?
Diminuiria o código, mas aí daria certo apenas para listas de objetos JSON e não resolveria o problema do crescente ou decrescente. Nós queremos um Ordenador que ordene qualquer coisa. Uma lista de String, uma lista de int, uma lista de objetos Pessoa (por exemplo) pela propriedade altura, uma lista de objetos Pessoa pela propriedade peso, uma lista de cão chupando manga pelo tamanho do chifre, uma lista de qualquer coisa por qualquer critério e em qualquer ordem.
E essa é a melhor parte: queremos isso em apenas um único método.
Mas, por enquanto, vamos ver se ordenamos por estilo também.
Edite a classe DataService e modifique o método ordenarEstadoAtual.
class DataService{
//(...)
void ordenarEstadoAtual(String propriedade){
List objetos = tableStateNotifier.value['dataObjects'] ?? [];
if (objetos == []) return;
Ordenador ord = Ordenador();
var objetosOrdenados = [];
final type = tableStateNotifier.value['itemType'];
if (type == ItemType.beer && propriedade == "name"){
objetosOrdenados = ord.ordenarCervejasPorNomeCrescente(objetos);
}else if (type == ItemType.beer && propriedade == "style"){
objetosOrdenados = ord.ordenarCervejasPorEstiloCrescente(objetos);
}
emitirEstadoOrdenado(objetosOrdenados, propriedade);
}
//(...)
}
Salve, rode o app e veja o funcionamento.
Para a próxima aula, você precisará refletir e debater sobre o porquê de eu ter escrito que chamar o objeto dataService ali de dentro do DataTableWidget era seboso. Não me decepcione.
Implemente todas as ordenações possíveis nesse estilo proposto pela receita, replicando código.
Você já treinou conhecimentos, ao longo das receitas, para retirar essa replicação de código sem apelar para o método de ordenação que o dart oferece. Tente fazer isso! Lembre que ordenação tem que ser feita via seu Ordenador.