Objeto: criação de um componente gráfico com herença de implementação.
Principais termos técnicos abordados: herança, classes, objetos, construtores, map, named parameters, widget, flutter
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
Analisemos o seguinte app:
Trata-se de nosso já bem conhecido MaterialApp, com um Scaffold dentro dele. Vamos ver o código para analisarmos melhor.
void main() {
MaterialApp app = MaterialApp(
theme: ThemeData(primarySwatch: Colors.deepPurple),
home: Scaffold(
appBar: AppBar(title: Text("Dicas")),
body: 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"),
)
]),
bottomNavigationBar: BottomNavigationBar(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))
]),
));
runApp(app);
}
Se você tem seguido as receitas direitinho, esse código não deve ser de muita dificuldade pra você ler. Mas esse app apresenta diversos problemas sobre os quais nos dedicaremos nesta e nas próximas atividades, então preste atenção.
O primeiro problema é que eu estou usando componentes pré-definidos pra fazer tudo. É excelente ter todos esses Widgets, todas essas caixas pra montar a interface com o usuário, mas uma interface mais complexa vai virar rapidamente um balaio de gato conforme mais e mais componentes sejam inclusos e, sobretudo, a gente fica sem ter onde programar o código que faz algumas coisas acontecerem. Por exemplo, se a gente tocar nos botões do painel de baixo não acontece absolutamente nada, e por uma razão bem simples: a gente não programou nada para acontecer (pode tentar executar o código para conferir). E a coisa mais besta que poderia acontecer não acontece: deixar selecionado o botão em que tocamos. Está sempre selecionado o botão Cafés, independentemente de em que botão se toca ou se clica, e nem cafés a gente está mostrando no painel principal.
Há certamente outros problemas, mas vamos nos concentrar em parte deste que acabamos de descrever.
E para resolver esse problema, não seria legal se a gente pudesse programar nossa própria caixa? Nas palavras corretas, seria programar nossa própria classe. Vamos fazer isso, passo a passo, errando e errando para acertar lá na frente.
Abra um projeto flutter e acrescente o seguinte código:
void main() {
MaterialApp app = MaterialApp(
theme: ThemeData(primarySwatch: Colors.deepPurple),
home: Scaffold(
appBar: AppBar(title: Text("Dicas")),
body: 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"),
)
]),
bottomNavigationBar: CaixaDeIconesQueEuMesmoProgramei()),
));
runApp(app);
}
Veja que fica até mais fácil da gente ler, não é mesmo?
Sendo que o compilador deve empombar com você, exibindo alguma mensagem como "The function 'CaixaDeIconesQueEuMesmoProgramei' isn't defined".
O compilador exibe essa mensagem porque você não programou a classe CaixaDeIconesQueEuMesmoProgramei. Ele tá se referindo a isso como função porque a fábrica é uma função - um construtor é uma função, uma função que escrevemos numa classe.
No mesmo arquivo, acrescente a classe CaixaDeIconesQueEuMesmoProgramei. Seu código completo deve ficar assim.
class CaixaDeIconesQueEuMesmoProgramei {
CaixaDeIconesQueEuMesmoProgramei();
}
void main() {
MaterialApp app = MaterialApp(
theme: ThemeData(primarySwatch: Colors.deepPurple),
home: Scaffold(
appBar: AppBar(title: Text("Dicas")),
body: 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"),
)
]),
bottomNavigationBar: CaixaDeIconesQueEuMesmoProgramei(),
));
runApp(app);
}
- Muito legal, professor, mas o compilador continua empombando...
Continua mesmo, e com razão. A mensagem deve ser essa aqui: The argument type 'CaixaDeIconesQueEuMesmoProgramei' can't be assigned to the parameter type 'Widget?'.
Isso quer dizer, basicamente, que não é qualquer caixa que você pode atribuir ao bottomNavigationBar do Scaffold. A caixa que sua fábrica cria tem que ser um Widget. E sua fábrica CaixaDeIconesQueEuMesmoProgramei() cria um objeto da classe CaixaDeIconesQueEuMesmoProgramei, que por enquanto não tem absolutamente nenhuma relação com a classe Widget.
Mas o fato é que sua classe tem que ser um Widget para que objetos dela caibam ali no bottomNavigationBar. E, na verdade, em sua classe sendo um Widget, uma caixa/objeto dela vai caber em praticamente qualquer canto da interface gráfica.
A grande questão é: como eu faço para que minha classe seja um Widget?
A coisa é mais simples de escrever do que de explicar, então vamos escrever primeiro e explicar em seguida.
Edite seu código.
class CaixaDeIconesQueEuMesmoProgramei extends StatelessWidget{
CaixaDeIconesQueEuMesmoProgramei();
}
void main() {
MaterialApp app = MaterialApp(
theme: ThemeData(primarySwatch: Colors.deepPurple),
home: Scaffold(
appBar: AppBar(title: Text("Dicas")),
body: 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"),
)
]),
bottomNavigationBar: CaixaDeIconesQueEuMesmoProgramei(),
));
runApp(app);
}
Bem, para sua classe ser um Widget não precisa mais que escrever isso no cabeçalho dela: extends StatelessWidget.
StatelessWidget é uma classe existente, um Widget, um componente gráfico, como todos os outros que vimos. O nome desse mecanismo é herança e está no coração da orientação a objetos. Uma ruma de código de interface gráfica que você teria que escrever para tornar sua classe um componente gráfico você não precisa mais escrever. Porque ao codificar extends StatelessWidget você herda todas as informações e comportamento presentes na classe StatelessWidget. Em síntese, essa linha torna sua classe um Widget e, por conseguinte, todas as caixas/objetos criadas por sua fábrica serão Widgets. Você faz parte do clubinho agora e o flutter, o tal framework de que falamos na receita anterior, saberá renderizar objetos de sua classe. Mas tem uma coisinha a mais.
Sempre tem uma ou duas coisinhas a mais.
A gente resolveu fazer nossa própria classe pra ter mais liberdade de codificar funcionalidades, sendo que até agora a gente não botou praticamente nenhum código nela.
Que tal escrevermos um código para montar e retornar aquela barra de botões?
Edite seu código.
class CaixaDeIconesQueEuMesmoProgramei extends StatelessWidget {
CaixaDeIconesQueEuMesmoProgramei();
BottomNavigationBar montar() {
return BottomNavigationBar(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))
]);
}
}
void main() {
MaterialApp app = MaterialApp(
theme: ThemeData(primarySwatch: Colors.deepPurple),
home: Scaffold(
appBar: AppBar(title: Text("Dicas")),
body: 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"),
)
]),
bottomNavigationBar: CaixaDeIconesQueEuMesmoProgramei(),
));
runApp(app);
}
No código em destaque estamos acrescentando uma função/método que monta e retorna uma caixa BottomNavigationBar do mesmo jeitinho da que a gente tem que exibir. Se os componentes gráficos chamassem esse método, a gente voltaria ao cenário inicial, mas com uma classe nossa para programar o que a gente bem entendesse.
Mas pense nos caras que programaram os demais Widgets. Eles não teriam como prever que o método a ser chamado para montar a interface gráfica do seu componente seria montar, não é mesmo? Outro programador poderia escolher outro nome, e a classe pode ter diversos outros métodos que não servem exatamente para montar a interface gráfica do seu Widget. Mas esses programadores poderiam muito bem estabelecer o método que você tem que implementar para que os objetos da sua classe funcionem com os outros componentes gráficos.
O compilador já está dando a dica: "Missing concrete implementation of 'StatelessWidget.build'.
Isso significa que o método que você tem que implementar se chama build.
Reescreva seu código.
class CaixaDeIconesQueEuMesmoProgramei extends StatelessWidget {
CaixaDeIconesQueEuMesmoProgramei();
@override
Widget build(BuildContext context) {
return BottomNavigationBar(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))
]);
}
}
void main() {
MaterialApp app = MaterialApp(
theme: ThemeData(primarySwatch: Colors.deepPurple),
home: Scaffold(
appBar: AppBar(title: Text("Dicas")),
body: 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"),
)
]),
bottomNavigationBar: CaixaDeIconesQueEuMesmoProgramei(),
));
runApp(app);
}
- Vige Maria... E esse @override? E esse Widget?? E esse BuildContext???
Bem, simplificando um pouco as coisas podemos dizer que quem programou a classe de quem estamos herdando (StatelessWidget) escreveu o método build nela, e escreveu dessa forma, com esse tipo de retorno e com esse parâmetro. Mas escreveu sem implementação. Se a gente quiser fazer parte do clubinho, se quiser encaixar nossos próprios componentes em outros componentes, precisamos implementar na nossa classe, o build que foi estabelecido lá no StatelessWidget mas que não foi implementado. Daí o uso do @override. Estamos indicando para o compilador que o método que segue substitui (ou sobrescreve) um método que foi definido numa das classes de que herdamos.
O fato de retornar um Widget, que é uma classe mais genérica do que o próprio StatelessWidget, permite que você retorne, rigorosamente, qualquer composição de componentes gráficos que você quiser.
- E o tal BuildContext?
É um parâmetro que será útil mais adiante, se quisermos interagir com o emaranhado de objetos que compõe a interface com o usuário. Por enquanto, encare como uma burocracia.
Execute o main e veja que temos a mesma interface gráfica do início da receita.
- E os problemas do início da receita? Mesmo com as mudanças ainda estamos com os mesmos problemas.
Estamos sim, efetivamente. Mas abrimos um espaço adequado para programar e resolvê-los, e devemos abrir outros espaços. Em especial, temos a classe CaixaDeIconesQueEuMesmoProgramei. Nela podemos codificar o que bem entendemos e poderemos, por exemplo, chamar uma função escrita por nós após os toques nos ícones. Essa função pode deixar selecionado o último botão tocado, por exemplo.
Vamos começar a fazer isso.
Modifique seu código.
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))
]);
}
}
void main() {
MaterialApp app = MaterialApp(
theme: ThemeData(primarySwatch: Colors.deepPurple),
home: Scaffold(
appBar: AppBar(title: Text("Dicas")),
body: 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"),
)
]),
bottomNavigationBar: NewNavBar(),
));
runApp(app);
}
Bem, em primeiro lugar a gente não podia ficar chamando um componente gráfico de CaixaDeIconesQueEuMesmoProgramei, por mais que o nome seja didático. Modifiquei para NewNavBar. Você pode chamar do que quiser, mas tente guardar sua criatividade pra escrever poemas - no código, pense num nome que descreva o que um objeto daquela classe será.
Em segundo lugar, definimos uma função que recebe um inteiro como parâmetro e faz um print.
(...)
void botaoFoiTocado(int index) {
print("Tocaram no botão $index");
}
(...)
Terceiro, na fábrica da caixa BottomNavigationBar, dissemos para ela chamar nossa função quando alguém tocasse em algum item.
(...)
return BottomNavigationBar(onTap: botaoFoiTocado,
(...)
Na prática, a gente botou nossa função botaoFoiTocado dentro da caixa/objeto BottomNavigationBar. Lembre que objetos têm informações e funções dentro de si. Nada impede que as fábricas nos permitam passar informações e também funções para por dentro da caixa sendo construída.
Rodando o código por uma IDE de desenvolvimento, você deve ver, no terminal, mensagens como "Tocaram no botão 0", "Tocaram no botão 1" e "Tocaram no botão 2", conforme acionados os botões de seu componente.
Ainda não é exatamente o que queremos fazer, mas estamos tomando controle da situação, estamos programando componentes, e não apenas montando componentes já existentes. Mais adiante aprenderemos a deixar o botão selecionado e fazer coisas bem mais emocionantes.
Por enquanto, precisamos treinar a produção de mais StatelessWidgets.
Faça com o componente que está no body do Scaffold o mesmo procedimento que fizemos com o componente que estava no bottomNavigationBar: crie uma nova caixa pra ele (dê um nome legal), mantendo a mesma funcionalidade.
Realize o mesmo procedimento com o objeto app no programa principal. A linha de criação dele deve ficar apenas MyApp app = MyApp(). E MyApp é uma classe que você deve criar, que herda de StatelessWidget e cujo método build cria um MaterialApp com os demais componentes que você criou. Note como o programa fica mais fácil de ser lido.
Realize o mesmo procedimento com o componente que está no appBar do Scaffold. Parece moleza como os outros exercícios, mas meio que não é. O problema é o seguinte: quem programou o Scaffold estabeleceu que os objetos que a gente pode por no atributo appBar não um pouco mais específicos que um StatelessWidget. Assim, herdar de StatelessWidget não funcionaria. A solução é emocionante: a gente sabe que ali naquele atributo cabe objeto da classe AppBar, então a gente pode criar uma classe que herda da própria classe AppBar. Assim, a gente vai ter certeza que objetos da nossa classe contemplam todas as especificidades necessárias para caber lá no atributo appBar do Scaffold. E dá para fazer isso sem nem precisar sobrescrever nenhum método, pois a classe AppBar já está toda implementadinha (tanto é que usamos objetos dela), ao contrário do StatelessWidget, em que falta a implementação do método build. Alguém já passou pela situação descrita neste exercício e botou a dúvida num fórum. Você deve ler as respostas para esta dúvida, identificar a melhor solução (normalmente, é a mais simples) e adequar a solução proposta para o seu cenário.
(desafio) Melhore o componente NewNavBar. Ele está muito restrito, pois sempre cria uma barra de navegação com 3 itens apenas e sempre os mesmos. Reescreva a fábrica na classe NewNavBar. Na nova fábrica, uma lista de Icon (os ícones) deve ser recebida como parâmetro (não se importe com rótulos/labels para esse exercício). O método build deve montar a barra completa, de acordo com os ícones passados para a fábrica. Restrição: não pode usar comandos de repetição explícitos.