Como utilizar temas no Flutter - Parte 1
A tematização ou estilização são nomes genéricos para o ato de fazer uma aplicação ser atrativa, com apelo ao usuário e que faça sentido com a marca da empresa ou o que ela precisa mostrar. É o que deixa a sua aplicação bonita e consegue, bastante, interferir até na acessibilidade para o usuário final.
O Flutter utiliza como padrão uma tematização ou estilo vinda do Material e baseada na cor azul. Toda pessoa que já fez um tutorial do Flutter já viu aquela app azul rodando, né?
Mas o que fazer quando queremos personalizar o nosso design como um todo?
Nessa primeira parte, vamos ver juntos como utilizar temas mais genéricos do Flutter para que você possa personalizar a sua app com a sua cara. Além disso, vamos aprender dois conceitos importantes: conceito de background e conceito de surface.
Já na segunda parte, você vai aprender a fazer temas específicos para cada widget ou cenário que você tenha na sua app, garantindo unicidade no desenvolvimento e ficando um passo mais próximo de ter um pequeno design system!
O que é um tema?
Um tema nada mais é do que um conjunto de cores e tipografias que tornam possível ajustar e aprimorar várias propriedades visuais em uma aplicação, como cores de fonte, cores de fundo e superfície, cores específicas de elementos de UI, etc.
Poderíamos considerar também os diferentes conjuntos de Widgets, Cupertino e Material, como extensões desses temas. Mas esse assunto vai ficar para próxima, ok?
No Flutter, customizar a sua aplicação para que ela tenha a aparência e transmita as mensagens certas é fundamental. A utilização de temas proporciona relações de hierarquia, fluxo e estrutura de toda a UI, além de auxiliar seu usuário a se engajar mais e até na sua aquisição de novos usuários.
O próprio Flutter tem um codelab focado em como fazer o seu app passar de monótono para lindo, caso você tenha interesse. A referência está aqui: Deixe seu app do Flutter lindo, não chato.
Conceitos de background e surface
Na minha experiência como dev, já sofri bastante para entender os conceitos de background e surface. Isso porque, no Flutter, sua árvore de widgets é construída em cima de uma base (geralmente um Scaffold) e tudo dela fica em cima dele (foreshadowing).
Antes de partirmos para a solução do problema, vamos primeiro entender direitinho o que ela resolve?
Quando você olha para o widget abaixo, qual você acredita que seja a cor de background?
Vou dar uma dica, aqui está o código desse Scaffold:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ListView(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 36, vertical: 12),
child: Card(
elevation: 3,
color: Colors.deepOrange,
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
),
),
],
),
);
}
Você pode pensar que é a cor laranja, pois ela é a cor do Card, que serve de fundo para os widgets de texto, né?
Mas agora olha esse cenário aqui:
Novamente, vamos ver o código:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ListView(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 36, vertical: 12),
child: Card(
elevation: 3,
color: Colors.deepOrange,
child: Padding(
padding: const EdgeInsets.all(24),
child: Row(
children: const [
Card(
elevation: 3,
color: Colors.white,
child: Padding(
padding: EdgeInsets.all(24),
child: Text(
'Conteudo do card 1',
),
),
),
SizedBox(width: 12),
Card(
elevation: 3,
color: Colors.white,
child: Padding(
padding: EdgeInsets.all(24),
child: Text(
'Conteudo do card 2',
),
),
),
SizedBox(width: 12),
Card(
elevation: 3,
color: Colors.white,
child: Padding(
padding: EdgeInsets.all(24),
child: Text(
'Conteúdo do card 3',
),
),
),
],
),
),
),
),
],
),
);
}
E agora? Temos a cor de fundo do Card, dos outros cards dentro dele, e também a cor de fundo da tela. Como poderemos identificar qual é qual? Para isso, é importante entender os conceitos de background e surface.
Ah, uma observação importante: aprendemos e definimos esses dois conceitos na Revelo, em conjunto com o nosso time de design da nossa app, e funcionou bastante bem! Ainda não encontrei nenhuma documentação mais profunda que comente sobre as árvores de widgets de Flutter. Fique à vontade para sugerir outras fontes para mim, tá? No final do texto você terá acesso aos meus contatos!
Conceito de background
Definimos que, para facilitar as nossas referências, o background é a camada mais inferior de uma UI. No caso do Flutter, o background será sempre a cor do Scaffold da tela. Voltando aos exemplos anteriores, a resposta sobre as cores de background seria: o branco atrás de todos os widgets.
Ilustrando para melhor entendimento:
Neste exemplo, tudo que fizemos foi aplicar uma backgroundColor ao scaffold.
Sim, ficou bem mais fácil aliar a definição ao que o Flutter já utiliza, né?
Conceito de surface
Com o conceito de background melhor definido, agora fica mais fácil explicar o conceito de surface. Uma surface pode ser toda superfície que esteja antes do elemento mais acima da árvore.
Imagine que na árvore, a camada mais profunda será o início dela, o Scaffold. Pense como se ele fosse a raiz. Ele será o background e, tudo acima dele, será uma surface. Vamos pensar no exemplo do card com outros cards dentro:
// Pensando em hierarquia da árvore de Widgets abaixo, temos:
// Scaffold (background, raiz da árvore) -> AppBar(surface)
// Scaffold (background, raiz da árvore) -> ListView(surface transparente, mas poderia ter cor) -> Card(surface laranja) -> Card(surface branca) -> Text (elemento mais acima da árvore)
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.amber, // este é o background
appBar: AppBar(
title: Text(widget.title),
backgroundColor: Colors.black, // esta é uma surface, apesar do nome dentro do Widget de AppBar ser backgroundColor
),
body: ListView(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 36, vertical: 12),
child: Card(
elevation: 3,
color: Colors.deepOrange, // esta é uma surface
child: Padding(
padding: const EdgeInsets.all(24),
child: Row(
children: const [
Card(
elevation: 3,
color: Colors.white, // esta é outra surface
child: Padding(
padding: EdgeInsets.all(24),
child: Text(
'Conteudo do card 1',
),
),
),
SizedBox(width: 12),
Card(
elevation: 3,
color: Colors.white, // esta é outra surface
child: Padding(
padding: EdgeInsets.all(24),
child: Text(
'Conteudo do card 2',
),
),
),
SizedBox(width: 12),
Card(
elevation: 3,
color: Colors.white, // esta é outra surface
child: Padding(
padding: EdgeInsets.all(24),
child: Text(
'Conteúdo do card 3',
style: TextStyle(
color: Colors.black, // esta é uma cor normal, presente no elemento mais acima da árvore.
),
),
),
),
],
),
),
),
),
],
),
);
}
E aí, deu pra entender? Acaba ficando simples né? Essa é a intenção mesmo: ser simples e fácil de entender e de aplicar.
Esses conceitos poderão te ajudar bastante quando for pensar em como implementar um sistema de design com temas na sua app, acredite em mim!
Utilizando ThemeData para personalizar sua aplicação Flutter
O Flutter utiliza como base padrão em suas apps o MaterialApp, que por sua vez utiliza a classe ThemeData para poder personalizar o tema (parâmetro theme do MaterialApp) da aplicação.
Recomendo novamente aqui que você faça o codelab do Flutter sobre temas: Deixe seu app do Flutter lindo, não chato.
A classe ThemeData
A classe ThemeData permite não só a personalização do tema padrão do MaterialApp, como mencionado anteriormente, mas também de árvores de widgets específicas dentro da app. Conseguimos atingir esses resultados de algumas maneiras. Vamos nessa?
Como utilizar o ThemeData
Para utilizarmos o ThemeData, temos dois jeitos:
1) Diretamente dentro do parâmetro theme do Material app:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter TextStyle demonstration'),
);
}
}
2) Dentro de uma árvore específica, envolta por um Widget chamado Theme
Theme(
data: ThemeData.from(
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.amber),
),
child: Builder(
builder: (BuildContext context) {
return Container(
width: 100,
height: 100,
color: Theme.of(context).colorScheme.primary,
);
},
),
)
Como você deve ter percebido, o primeiro trecho define o tema geral da aplicação e o segundo pode ser utilizado dentro de uma parte específica dela, como uma página ou um componente.
Para conseguirmos acessar um parâmetro do tema, utliizamos o getter Theme.of(context) para acessarmos o tema do contexto atual e encontrarmos o parâmetro correto.
Construtores do ThemeData
Temos 5 construtores padrão da classe ThemeData, que facilitam na criação de novos temas:
ThemeData(): É a factory mais ampla do ThemeData. Permite a definição de campos específicos que sejam necessários, completando os outros campos de acordo com o tema geral do Material. Com ele, podemos definir temas específicos para alguns Widgets que sejam necessários para a aplicação, como AppBarTheme, SnackBarThemeData, ProgressIndicatorThemeData, ButtonThemeData, CardTheme, DialogTheme, ElevatedButtonThemeData, e por aí vai.
ThemeData.from(): O ThemeData.from() é uma factory bem poderosa. Ele te permite a criação de um ThemeData inteiro a partir de duas informações: o colorScheme e o textTheme. Para saber mais sobre essas informações, recomendo que você visite a documentação delas: Documentação do ColorSchemeDocumentação do TextTheme. Como um exemplo de aplicações, seguem:
// tema a partir um ColorScheme.fromSwatch()
Theme(
data: ThemeData.from(
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.amber),
),
child: Container()
);
// tema a partir um ColorScheme.fromSeed() - recomendado pela documentação!
Theme(
data: ThemeData.from(
colorScheme: ColorScheme.fromSeed(
seedColor: Color.fromARGB(255, 66, 165, 245),
),
),
child: Container()
);
// tema a partir de um TextTheme
Theme(
data: ThemeData.from(
textTheme: const TextTheme(
bodyMedium: TextStyle(color: Colors.green),
),
),
child: Container()
);
ThemeData.light(): Este é o tema padrão com a cor Colors.blue como cor primária.
ThemeData.dark(): Este é um tema padrão mais escuro com a cor Colors.grey[900] como cor primária e Colors.tealAccent como cor secundária no ColorScheme.
ThemeData.raw(): Este construtor é uma constante e você precisará definir todos os campos do ThemeData para poder utilizá-lo. Particularmente, nunca vi uma aplicação utilizar este construtor, pois daria muito trabalho definir todos os temas possíveis que o Material precisa para um tema. A principal diferença deste raw() para o ThemeData normal é que os campos que são nullable no ThemeData são necessários para ele.
Aplicando temas em sub-árvores ou componentes específicos
Já aprendemos bastante coisa, mas tem mais um ponto importante sobre quando utilizamos o widget Theme que eu preciso te contar. Juro que tá acabando, ok?
Vamos pensar em uma aplicação que tenha o tema abaixo definido na sua raiz, no MaterialApp:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Theme Demo - Part 1'),
);
}
}
Agora, vamos fazer um teste alterando o ColorScheme dentro de um Widget Theme de acordo com o código abaixo, qual você acha que será o resultado? Os quadrados da esquerda ou os quadrados da direita?
Theme(
data: ThemeData.from(
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.green),
),
child: Center(
child: Container(
width: 100,
height: 100,
color: Theme.of(context).colorScheme.primary,
),
),
),
Theme(
data: ThemeData.from(
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.green),
),
child: Builder(
builder: (BuildContext context) {
return Center(
child: Container(
width: 100,
height: 100,
color: Theme.of(context).colorScheme.primary,
),
);
},
),
),
Se você respondeu os quadrados da esquerda, você acertou!
Para que o Container possa utilizar a cor correta, definida em um Theme que o envolva, é necessário que ele passe por um processo de build para que possa ter um contexto novo. E somente neste contexto novo a alteração do tema será aplicada.
Parece meio confuso né? Mas é assim mesmo -infelizmente. Eu não podia deixar passar essa informação.
Conclusão e código fonte
Bom, é isso! O ThemeData e suas aplicações finalizam a primeira parte dessa sequência sobre temas! Se você achou chato ter que usar um Builder toda vez que for utilizar um tema novo, fique tranquilo - na próxima parte veremos como podemos personalizar nossos temas ainda mais e nos livrar de alguns desses empecilhos. Estou animado, e você? Vamos nessa?
O código fonte utilizado para as imagens e códigos dessa parte estão disponíveis no link: Flutter Theme Demo - Part 1.
Espero que tenha conseguido te ajudar a incorporar alguns conceitos importantes sobre o que é background, o que é uma surface e como utilizar temas no Flutter. Me conta o que você achou e, se tiver dúvidas ou sugestões, pode me chamar no LinkedIn ou no e-mail que conversamos, tá bom? Obrigado!
A Revelo Content Network acolhe todas as raças, etnias, nacionalidades, credos, gêneros, orientações, pontos de vista e ideologias, desde que promovam diversidade, equidade, inclusão e crescimento na carreira dos profissionais de tecnologia.