Objeto: criação de um componente gráfico com herança de implementação.
Principais termos técnicos abordados: herança, classes, objetos, fábricas/construtores, List, map, named parameters, widget, função de alta ordem, função de callback
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
Vamos partir de um aplicativo-base. Se você cumpriu as receitas anteriores, não deverá ter dificuldade em entendê-lo.
Nosso objetivo é organizar melhor o código do aplicativo, acrescentando uma ou outra funcionalidade para que, no meio do caminho, a gente retrabalhe alguns conceitos importantes e introduza alguns novos. Debateremos classes, atributos, métodos, funções de callback e funções de alta ordem.
A interface do aplicativo é a mesma da receita anterior...
E o código inicial está aqui, abaixo.
Crie um novo projeto e cole o código.
import 'package:flutter/material.dart';
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: DataBodyWidget(),
bottomNavigationBar: NewNavBar(),
));
}
}
class NewNavBar extends StatelessWidget {
NewNavBar();
void botaoFoiTocado(int index) {
print("Tocaram no botão $index");
}
@override
Widget build(BuildContext context) {
return BottomNavigationBar(onTap: botaoFoiTocado, 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 DataBodyWidget extends StatelessWidget {
DataBodyWidget();
@override
Widget build(BuildContext context) {
return Column(children: [
Expanded(
child: Text("La Fin Du Monde - Bock - 65 ibu"),
),
Expanded(
child: Text("Sapporo Premiume - Sour Ale - 54 ibu"),
),
Expanded(
child: Text("Duvel - Pilsner - 82 ibu"),
)
]);
}
}
Nossa primeira missão é ajeitar a classe DataBodyWidget.
Eu vou continuar mais um pouco com minha "Teoria das Caixas" para falar de classes e objetos. Os objetos são caixas e as classes são modelos de caixas.
Assim, lembre que o DataBodyWidget é um modelo de caixa, e não uma caixa. Como modelo, ele define o que cabe dentro de cada caixa que vier a ser criada a partir dele. E o que pode caber dentro dessas caixas que poderão ser criadas são, basicamente, informação e comportamento. E uma fábrica, que é um comportamento especial, usado para criar um caixa com base no modelo.
Atualmente o código deste Widget está assim:
class DataBodyWidget extends StatelessWidget {
//a "fábrica"
DataBodyWidget();
//um comportamento
@override
Widget build(BuildContext context) {
return Column(children: [
Expanded(
child: Center(child: Text("La Fin Du Monde - Bock - 65 ibu")),
),
Expanded(
child: Center(child: Text("Sapporo Premiume - Sour Ale - 54 ibu")),
),
Expanded(
child: Center(child: Text("Duvel - Pilsner - 82 ibu")),
)
]);
}
}
A gente pode pensar que o nosso modelo de caixa prevê que nas caixas em si só caberá um comportamento, o método build, e nenhuma informação, uma vez que não há nenhuma variável declarada entre o cabeçalho da classe e a definição da fábrica e da função build.
Mas essa é uma leitura incorreta, pois pela linha...
class DataBodyWidget extends StatelessWidget
...nosso modelo define que as caixas criadas a partir dele meio que já vêm com um StatelessWidget "de brinde". Assim, mesmo que a gente não veja, uma caixa de nosso modelo de caixa tem todas as informações e todos os comportamentos implementados no modelo StatelessWidget.
Na programação, essa relação é conhecida como herança e, em a estabelecendo assim, nossas futuras caixas DataBodyWidget poderão ser enxergadas como se fossem uma caixa StatelessWidget. E, por isso mesmo, caberão em qualquer lugar da interface onde caiba uma caixa StatelessWidget. E isso é uma maravilha, pois permite que a gente encaixe nossas caixas - nossos objetos DataBodyWidget - em virtualmente qualquer lugar, porque tem muitos, muitos componentes gráficos que podem se compor com uma caixa do tipo StatelessWidget.
Em todo caso, cabe informação nesse nosso modelo.
Vamos tornar o DataBodyWidget menos "burro". Atualmente ele só exibe as mesmas coisas. Qualquer caixa que for criada exibirá sempre as mesmas informações. Que tal fazer sua fábrica receber uma lista de String e o método build montar sua interface com base nessa lista?
Edite sua classe DataBodyWidget. Cuidado para não apagar as outras classes!
class DataBodyWidget extends StatelessWidget {
List<String> objects;
DataBodyWidget( {this.objects = const [] });
@override
Widget build(BuildContext context) {
return Column(children: [
Expanded(
child: Center(child: Text("La Fin Du Monde - Bock - 65 ibu")),
),
Expanded(
child: Center(child: Text("Sapporo Premiume - Sour Ale - 54 ibu")),
),
Expanded(
child: Center(child: Text("Duvel - Pilsner - 82 ibu")),
)
]);
}
}
Vamos com calma que estamos fazendo muita coisa aqui. Primeiro declaramos um atributo objects...
List<String> objects;
É de se prever que nessa lista possa constar, por exemplo, os valores "La Fin Du Monde - Bock - 65 ibu", "Sapporo Premiume - Sour Ale - 54 ibu" e "Duvel - Pilsner - 82 ibu". Mas pode constar também qualquer outra lista de Strings, e de qualquer tamanho.
Depois definimos uma fábrica para nosso Widget.
DataBodyWidget( {this.objects = const [] });
Isso permite que quem for criar uma caixa do nosso modelo passe como parâmetro a lista de String que bem entender. Essa lista de String vai ser armazenada "dentro da caixa", na variável objects, que a gente chama de atributo. Por essa lista de String estar dentro da caixa, estará visível a todos os comportamentos implementados, inclusive, claro, o build. Se quem estiver criando a caixa não passar nada como parâmetro para a fábrica, uma lista vazia será posta dentro da caixa como valor default.
Bem, a questão agora é mais de algoritmo do que de OO.
O algoritmo do build tá montando uma interface estática...
(...)
@override
Widget build(BuildContext context) {
return Column(children: [
Expanded(
child: Center(child: Text("La Fin Du Monde - Bock - 65 ibu")),
),
Expanded(
child: Center(child: Text("Sapporo Premiume - Sour Ale - 54 ibu")),
),
Expanded(
child: Center(child: Text("Duvel - Pilsner - 82 ibu")),
)
]);
}
(...)
Mas a gente quer montar uma interface com base nos valores do atributo objects.
Basicamente, para cada String dentro de objects, a gente deve criar uma caixa Expanded com o valor do string dentro de um Text.
É descaradamente um algoritmo de repetição.
Vamos a uma solução correta, mas que ainda não é a ideal.
Substitua a classe DataBodyWidget.
class DataBodyWidget extends StatelessWidget {
List<String> objects;
DataBodyWidget( {this.objects = const [] });
@override
Widget build(BuildContext context) {
List<Expanded> allTheLines = [];
for (var obj in objects){
allTheLines.add(
Expanded(
child: Center(child: Text(obj)),
)
);
}
return Column(children: allTheLines);
}
}
Usamos um for para implementar nosso algoritmo. Não se apegue muito a esse for, já não é tão comum sua utilização.
Basicamente, para cada string na coleção objects, a gente criou uma caixa Expanded e empurrou-a numa lista de Expanded. Para percorrer uma coleção, a gente precisa "dizer" duas coisas para o for: a coleção que vai ser percorrida (objects) e a variável (obj) que vai receber um de seus elementos a cada volta do loop. O resto é sintaxe, o var declara obj como uma variável local (pode por String, se quiser) e o in separa a variável que vai receber o item da coleção de onde os itens são tirados. Tem linguagem em que esse in é of, tem linguagem em que esse in é sinal de dois pontos, em Dart é in.
for (var obj in objects)
Cada Expanded foi criado nos mesmos moldes da aplicação inicial, a diferença é que dentro do Text a gente põe os strings da lista objects em vez de um String fixo. No original, estava...
Expanded(
child: Center(child: Text("La Fin Du Monde - Bock - 65 ibu")),
)
...ao passo que na versão com laço ficou...
Expanded(
child: Center(child: Text(obj)),
)
Por fim, como na versão inicial a gente estava passando para o parâmetro children da fábrica de Column justamente uma lista de Expanded...
return Column(children: [
Expanded(
child: Center(child: Text("La Fin Du Monde - Bock - 65 ibu")),
),
Expanded(
child: Center(child: Text("Sapporo Premiume - Sour Ale - 54 ibu")),
),
Expanded(
child: Center(child: Text("Duvel - Pilsner - 82 ibu")),
)
]);
...basta-nos atribuir ao parâmetro a lista que a gente populou através do comando for.
List<Expanded> allTheLines = [];
for (var obj in objects){
allTheLines.add(
Expanded(
child: Center(child: Text(obj)),
)
);
}
return Column(children: allTheLines);
}
Agora vamos ver no que dá isso tudo.
Salve e execute o programa.
A interface deve ficar sem nada no conteúdo...
- Oxente, esse moído todin pra não aparecer nada...
Parece frustrante mesmo, mas na verdade é muito bonito e não aconteceu nada que a gente não tivesse programado.
- E porque não tá aparecendo nada?
Porque você, nobre empinador de pipa em ventilador, não percebeu que na hora de fabricar uma caixa DataBodyWidget a gente não passou nada para a fábrica. Dá uma olhada lá na classe MyApp...
(...)
home: Scaffold(
appBar: AppBar(
title: const Text("Dicas"),
),
body: DataBodyWidget(),
bottomNavigationBar: NewNavBar(),
));
(...)
- Ah, entendi. Mas isso é bonito por quê?
Porque a gente programou de forma a proteger nosso componente DataBodyWidget, a gente o tornou "fool proof", ou à prova de caba besta, numa tradução anordestinada. Mesmo que o cara esqueça de passar parâmetros para a fábrica, a coisa funciona, apenas não exibe nada (o cara não passou nada, afinal). E, sobretudo, não dá crash no app. A magia acontece aqui, na definição da fábrica:
DataBodyWidget( {this.objects = const [] });
As chaves envolvendo o parâmetro significam que o parâmetro ali descrito é um named parameter - quando alguém for chamar a fábrica, vai ter que especificar o nome do parâmetro e não apenas seu valor. O this.objects significa que, quando alguém chamar a fábrica, por exemplo, com DataBodyWidget(objects: ["Maria", "Preá"]), a fábrica vai por o valor ["Maria", "Preá"] na propriedade objects da caixa que está sendo criada - nesse ponto, o this se refere a isso, ao objeto/caixa em criação. Note que a gente não precisa operação de atribuição, o compilador vai entender o que a gente quer e gerar a atribuição para a gente. Já o = const [] está dando um valor default para o atributo objects caso quem chamar a fábrica esqueça de passar esse parâmetro, como foi o nosso caso. Talvez você não perceba, mas isso evita um if, lá no método build, pra testar se o parâmetro é null, porque conforme a gente programou ele nunca vai ser null - se quem chama a fábrica não passar nada, ele vai ser uma lista constante e vazia, mas não nula.
Mas o que a gente quer mesmo é ver os dados lá na interface gráfica, não é mesmo?
Substitua a classe MyApp. Cuidado para não mexer com o restante do código!
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: DataBodyWidget(objects:[
"La Fin Du Monde - Bock - 65 ibu",
"Sapporo Premiume - Sour Ale - 54 ibu",
"Duvel - Pilsner - 82 ibu"
]),
bottomNavigationBar: NewNavBar(),
));
}
}
Agora sim, voltamos, enfim, ao início. Mas com um componente mais legal, mais genérico.
Mas falta alguma coisa ainda lá naquele build do DataBodyWidget...
(...)
Widget build(BuildContext context) {
List<Expanded> allTheLines = [];
for (var obj in objects){
allTheLines.add(
Expanded(
child: Center(child: Text(obj)),
)
);
}
return Column(children: allTheLines);
}
(...)
Eu escrevi lá atrás para você não se apegar muito àquele for, não escrevi? Pois bem, haveremos de nos livrar dele.
Porque, se a gente analisar com calma este algoritmo que implementamos, a gente percebe que estamos transformando uma lista em outra lista. É, digamos, um algoritmo de "mapeamento", no sentido de que cada objeto do vetor original é mapeado num elemento correspondente do vetor resultando através de algum processamento - nesse caso, o processamento é a criação de objetos Expanded.
Assim, estamos processando a lista (de strings) objects e montando a lista (de Expanded) allTheLines. E temos ainda algumas peculiaridades:
não modificamos a lista original, apenas processamos, fazemos operações com seus elementos
a lista que é resultado do processamento tem exatamente o mesmo tamanho da lista que o algoritmo processa
cada elemento da lista resultante é obtido exclusivamente do processamento de um elemento correspondente na lista processada. O elemento 0 da lista resultante é obtido exclusivamente a partir do elemento 0 da lista processada e assim por diante
Acontece que um algoritmo com essas características é absurdamente comum, tão comum que alguém já o generalizou e o transformou numa função "de biblioteca". Toda lista, nas linguagens mais modernas, tem uma função que "faz um for" em seus elementos, processa-os, empurra-os numa lista resultante e retorna essa nova lista - exatamente o que a gente quer fazer. A coisa vai ficar mais ou menos assim, simplificando e aportugesando.
(...)
Widget build(BuildContext context) {
List<Expanded> allTheLines = objects.facaNovaListaComBaseEmMeusElementos();
return Column(children: allTheLines);
}
(...)
O cara legal que implementou essa função facaOutraListaComBaseEmMeusElementos, no entanto, pôde prever quase tudo do que a gente precisa. Pôde prever que a gente precisa de uma lista resultante, que a gente vai fazer um laço e que cada elemento da lista processada vai gerar um elemento correspondente na lista resultante. O que ele não pôde prever é o processamento que deve ser feito para transformar UM elemento da lista original em UM elemento da lista resultante. Então esse cara legal exige que a gente passe como parâmetro uma função que sabe fazer isso. Ficaria assim:
(...)
Expanded processarUmElemento(String obj){
return Expanded(
child: Center(child: Text(obj)),
);
}
@override
Widget build(BuildContext context) {
List<Expanded> allTheLines = objects.facaNovaListaComBaseEmMeusElementos(processarUmElemento);
return Column(children: allTheLines);
}
(...)
Note que estamos passando a definição da função processarUmElemento e não o resultado de uma chamada a essa função. E o que essa função faz, quando invocada? Recebe um elemento da lista original como parâmetro e retorna um elemento correspondente da nova lista. Mas note que estamos passando essa função como se fosse uma variável. E quem vai chamar essa função? A função facaNovaListaComBaseEmMeusElementos, quando estiver fazendo o laço para montar a nova lista.
Essas características dão dois apelidos famosos para essas funções. A função facaNovaListaComBaseEmMeusElementos recebe A DEFINIÇÃO de uma outra função como parâmetro, chamamos esse tipo de função de função de alta ordem. Já a função processarUmElemento tem sua definição passada como parâmetro para ser invocada por uma função de alta ordem. Chamamos esse tipo de função de função de callback. Já vi quem chamasse de função de primeira classe. Aqui vamos chamar de função de callback mesmo.
Mas, voltando ao cara legal que escreveu a função de alta ordem pra gente usar, ele não foi legal o suficiente. Ele não chamou a função de facaNovaListaComBaseEmMeusElementos, conforme você deve ter desconfiado. Ele a chamou de map.
Substitua o componente DataBodyWidget pelo código a seguir.
class DataBodyWidget extends StatelessWidget {
List<String> objects;
DataBodyWidget( {this.objects = const [] });
Expanded processarUmElemento(String obj){
return Expanded(
child: Center(child: Text(obj)),
);
}
@override
Widget build(BuildContext context) {
List<Expanded> allTheLines = objects.map(processarUmElemento);
return Column(children: allTheLines);
}
}
- Ah, mas ainda tá dando erro aqui.
Calma que o Brasil é nosso. Estamos quase lá. É que o cara legal faltou ser um pouco mais legal ainda. Ele implementou a função map pra retornar não uma lista, mas uma outra coleção de objetos. O tipo exato dessa outra coleção não nos interessa agora, o que interessa é que a gente é que nem menino buchudo fi de rico e a gente quer porque quer uma lista pra poder encaixar lá no atributo children do Column. Felizmente, essa outra coleção retornada pelo map sabe se transformar numa lista.
Modifique sua classe.
class DataBodyWidget extends StatelessWidget {
List<String> objects;
DataBodyWidget( {this.objects = const [] });
Expanded processarUmElemento(String obj){
return Expanded(
child: Center(child: Text(obj)),
);
}
@override
Widget build(BuildContext context) {
List<Expanded> allTheLines = objects.map(processarUmElemento).toList();
return Column(children: allTheLines);
}
}
Falta bem pouco agora. Notemos, por fim, que essa função de callback, a função processarUmElemento, ela tem a características de uma função pura. Uma função pura não é uma função que nunca brincou o Carnaval no Magão, trata-se apenas de uma função que não depende de absolutamente nada que não sejam seus próprios parâmetros. A função build, por exemplo, já seria uma função bandoleira, porque usa a lista objects, que não é parâmetro dela.
Mas o que importa é que a função processarUmElemento é uma função de callback, é usada apenas naquele ponto específico e é uma função pura. Quando uma função reúne todas essas características não há razão para que se dê um nome a ela - ela pode ser definida na mesma hora em que se chama a função de alta ordem.
Edite seu código.
class DataBodyWidget extends StatelessWidget {
List<String> objects;
DataBodyWidget( {this.objects = const [] });
Expanded processarUmElemento(String obj){
return Expanded(
child: Center(child: Text(obj)),
);
}
@override
Widget build(BuildContext context) {
List<Expanded> allTheLines = objects.map(
(obj) => Expanded(
child: Center(child: Text(obj)),
)
).toList();
return Column(children: allTheLines);
}
}
Pois é, aquilo em negrito é um forma sintética de se definir uma função. A coisa deve funcionar, e a interface fica do jeito que a gente quer.
Então, execute seu programa e veja por si.
Se você quiser acanalhar mesmo e mostrar que sabe programar, dá pra fazer tudo na função build de uma lapada só, resumindo ainda mais o código.
Edite sua classe.
class DataBodyWidget extends StatelessWidget {
List<String> objects;
DataBodyWidget( {this.objects = const [] });
Expanded processarUmElemento(String obj){
return Expanded(
child: Center(child: Text(obj)),
);
}
@override
Widget build(BuildContext context) {
return Column(children: objects.map(
(obj) => Expanded(
child: Center(child: Text(obj)),
)
).toList());
}
}
Tudo deve rodar normalmente.
Seu código final deve ficar algo assim:
import 'package:flutter/material.dart';
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: DataBodyWidget(objects:[
"La Fin Du Monde - Bock - 65 ibu",
"Sapporo Premiume - Sour Ale - 54 ibu",
"Duvel - Pilsner - 82 ibu"
]),
bottomNavigationBar: NewNavBar(),
));
}
}
class NewNavBar extends StatelessWidget {
NewNavBar();
void botaoFoiTocado(int index) {
print("Tocaram no botão $index");
}
@override
Widget build(BuildContext context) {
return BottomNavigationBar(onTap: botaoFoiTocado, 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 DataBodyWidget extends StatelessWidget {
List<String> objects;
DataBodyWidget( {this.objects = const [] });
Expanded processarUmElemento(String obj){
return Expanded(
child: Center(child: Text(obj)),
);
}
@override
Widget build(BuildContext context) {
return Column(children: objects.map(
(obj) => Expanded(
child: Center(child: Text(obj)),
)
).toList());
}
}
Em tempo: função pura é uma definição que existe mesmo no mundo da programação. Função bandoleira, eu inventei.
Vamos aos exercícios.
O mesmo procedimento que foi feito com o DataBodyWidget, faça-o com o NewNavBar. Ele deve receber uma lista de ícones e montar a barra com eles (não se preocupe com os labels dos itens da barra, por enquanto)
Acrescente um menu daqueles de 3 botões verticais no appBar. As opções do menu devem ser cores. Futuramente, mudaremos a cor do tema com esse menu. Por enquanto, apenas monte a interface.
Se o appBar ficar grande demais, mova-o para uma classe sua. Você pode ter que estender o próprio AppBar em vez do StatelessWidget. Desenrole.