Objeto: modificação de um componente do app para que ele possa trabalhar com "data objects".
Principais termos técnicos abordados: interface, polimorfismo, data objects, named constructor
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 vez mais, vamos partir exatamente de onde paramos na receita anterior, então abra logo o código resultante dela.
A gente vai endereçar dois pontos particulares que podem vir a ser problemáticos num sistema maior do que esse que estamos implementando.
Primeiramente, nossa classe DataService, que acessa a API, só entende JSON - lê JSON e passa JSON adiante. Nesses objetos JSON estão os dados do app, e num app dessas dimensões talvez seja melhor deixar que os dados fiquem em objetos JSON mesmo, mas num sistema maiorzinho a gente vai precisar que os dados estejam em objetos de classes convencionais. Nesse nosso app, mais especificamente, a gente precisaria das classes Beer, Nation e Coffee, ou versões em português delas. Os objetos dessas classes vão fazer, basicamente, o que os objeto JSON fazem, armazenar dados. Por isso esses objetos são chamados de data objects (são conhecidos também por "objetos de modelo"). No entanto, em apps mais realistas essas classes podem e devem "fazer conta" também. Onde tem informação, pode ter processamento.
Segundamente, o nossa classe DataTableWidget trabalha apenas com listas de objetos JSON. Atente para o trecho em destaque...
(...)
@override
Widget build(BuildContext context) {
return DataTable(
(...)
rows: jsonObjects.map(
(obj) => DataRow(
cells: propertyNames.map(
(propName) => DataCell(
Text(obj[propName])
)
).toList()
)
).toList());
}
Essa notação (indexação) serve para acessar dados de objetos JSON, que são dicionários, afinal de contas. Mas não serve para acessar dados de um objeto de uma classe convencional. Podemos usar polimorfismo para resolver isso. O mesmo vale para o DataService, que só trabalho correntemente com JSON e lista de JSON.
Vamos primeiro resolver o primeiramente, para, nos exercícios, partirmos para o segundamente.
No diretório data, crie um arquivo de nome models.dart.
A gente deve por as classes Beer, Nation e Coffee nesse arquivo. No corpo da receita, trabalharemos com Beer apenas.
Edite o arquivo models.dart.
class Beer{
String name, style, ibu;
Beer({required this.name,required this.style,required this.ibu});
}
class Coffee{
String blendName, origin, variety;
Coffee({required this.blendName, required this.origin, required this.variety});
}
class Nation{
String nationality, capital, language, nationalSport;
Nation({required this.nationality, required this.capital, required this.language, required this.nationalSport});
}
- Só isso, Professor?
Bem, considerando que a gente pode usar JSON e não escrever porra nenhuma, eu acho até muito.
E ainda vai ter mais coisa.
- Sempre tem.
Sempre.
Por mais que a gente tenha criado essas classes de modelo, a API que a gente acessa nos dá texto no formato JSON. E a gente, claro, não quer escrever o código de conversão de texto para objetos dessas classes.
A gente não quer, assim como ninguém quer.
Mas a realidade é que a gente precisa de, ao menos, 2 métodos utilitários em nossas classes de modelo para compatibilizar operações com JSON:
um método construtor fromJSON que receba um objeto JSON e retorne um objeto de modelo com as propriedades devidamente preenchidas
um método toJSON que retorne uma versão JSON do objeto de modelo
Por vezes a gente precisa de um método copy (ou copyWith), que recebe um JSON como parâmetro e copia seus valores para as propriedades do objeto de modelo. Nesse nosso app, especificamente, a gente vai precisar apenas do construtor. Vamos implementar, também e a título ilustrativo, os demais métodos.
Edite o arquivo models.dart, acrescentando a classe Beer.
class Beer{
String name, style, ibu;
Beer({this.name='',this.style='',this.ibu=''});
Beer.fromJson(Map<String, dynamic> json):
name = json['name'] ?? '',
style = json['style'] ?? '',
ibu = json['ibu'] ?? '';
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = Map<String, dynamic>();
data['name'] = this.name;
data['style'] = this.style;
data['ibu'] = this.ibu;
return data;
}
void copy(Map<String, dynamic> json){
name = json['name'] ?? name;
style = json['style'] ?? style;
ibu = json['ibu'] ?? ibu;
}
}
Note que é um código bastante burocrático, e coisa bem parecida vai precisar ser feita com as outras duas classes de modelo, conforme veremos.
Aquele Beer.fromJson(Map<String, dynamic> json) é o que a gente chama de named constructor, um construtor com um nome diferente do nome da classe. Nesse caso, a gente não poderia declarar um segundo construtor, mesmo recebendo parâmetros diferentes do primeiro. Isso seria uma sobrecarga de construtores (o mesmo nome, parâmetros distintos), e Dart não permite esse tipo de coisa, que é bastante comum em Java. Dart também não permite sobrecarga de método convencional (ter vários métodos com o mesmo nome numa classe e distinção na listagem de parâmetros).
Agora, modifique a classe Coffee.
class Coffee{
String blendName, origin, variety;
Coffee({this.blendName='', this.origin='', this.variety=''});
Coffee.fromJson(Map<String, dynamic> json):
blendName = json['blend_name'] ?? '',
origin = json['origin'] ?? '',
variety = json['variety'] ?? '';
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = Map<String, dynamic>();
data['blend_name'] = this.blendName;
data['origin'] = this.origin;
data['variety'] = this.variety;
return data;
}
void copy(Map<String, dynamic> json){
blendName = json['blend_name'] ?? blendName;
origin = json['origin'] ?? origin;
variety = json['variety'] ?? variety;
}
}
Note que o código em destaque serve para transformar um objeto da classe Coffee num JSON. Tem um método desses na classe Beer também.
Por fim, modifique a classe Nation.
class Nation{
String nationality, capital, language, nationalSport;
Nation({this.nationality='', this.capital='', this.language='', this.nationalSport=''});
Nation.fromJson(Map<String, dynamic> json):
nationality = json['nationality'] ?? '',
capital = json['capital'] ?? '',
language = json['language'] ?? '',
nationalSport = json['national_sport'] ?? '';
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = Map<String, dynamic>();
data['nationality'] = this.nationality;
data['capital'] = this.capital;
data['language'] = this.language;
data['national_sport'] = this.nationalSport;
return data;
}
void copy(Map<String, dynamic> json){
nationality = json['nationality'] ?? nationality;
capital = json['capital'] ?? capital;
language = json['language'] ?? language;
nationalSport = json['national_sport'] ?? nationalSport;
}
}
Note que o código em destaque serve para copiar valores de um JSON recebido como parâmetro para as propriedades correspondentes de um objeto Nation. Se no parâmetro não houver a propriedade, o valor da propriedade do objeto Nation não é alterado, é basicamente para isso que estamos empregando o operador ??
Esse site gera a classe de modelo a partir de um exemplo de JSON. Gera com alguns defeitos, mas ainda poupa algum trabalho.
Tudo está muito bonitinho até agora, mas o fato é que nossa classe DataService não trabalha com objetos dessas classes. Você deve criar um novo DataService que faça isso: acesse a API e transforme a lista de JSON em lista de Beer ou de Coffee ou de Nation. Sua interface gráfica vai ter que funcionar com essas novas listas também. Dica: usar polimosfismo vai simplificar bastante suas soluções.