Como utilizar temas no Flutter - Parte 2

Como utilizar temas no Flutter - Parte 2

Na primeira parte dessa série sobre como utilizar temas no Flutter, vimos alguns conceitos básicos de temas, aprendemos os conceitos de surface e background e utilizamos o ThemeData para personalizar as nossas aplicações.

Essa utilização mais básica pode ser bem legal para projetos pessoais ou pequenos que não tenham muitos desenvolvedores envolvidos no desenvolvimento da UI da aplicação.

Quando a aplicação começa a ganhar volume de usuários, código e também relevância nas lojas, torna-se cada vez mais importante pensarmos em como deixá-la mais fácil de dar manutenção, de ser entendida por todos os envolvidos no projeto e, também, como evitar que sejam utilizados tokens diferentes para as mesmas funções ou contextos.

Pensando nisso, como podemos fazer se temos uma aplicação grande ou um sistema de design complexo e precisamos garantir que todas as pessoas envolvidas no projeto utilizem as mesmas cores e estilos de texto?

Vem comigo que hoje vamos falar sobre temas personalizados de maneira mais avançada! Are you ready? Let's go!

Um pequeno disclaimer

Por conta da complexidade do assunto, utilizarei como base para este artigo as práticas aplicadas nas aplicações Flutter da Revelo e aprendizados que tivemos ao longo do tempo.

Utilizando tokens para um tema geral de aplicação

Na Revelo, organizamos nossos temas a partir de duas classes de tokens: Colors e Typography. Isso nos ajudará a ter esses tokens (de cor e texto) centralizados em um só lugar, tornando sua manutenção mais fácil. Já passamos por dois rebrandings na nossa app e essas classes ajudaram muito!

Colors

Uma classe Colors será composta pelos tokens de cor que o seu time de design definiu e será a fonte da verdade em relação a eles. Idealmente, você deve evitar utilizar cores que não estejam nesta classe, ou seja, não foram definidas pelo seu time de design. Também recomendo como uma boa prática evitar utilizar esses tokens diretamente nos seus widgets, mas sim utilizar a camada de temas para isso, ok?

Por aqui tenho duas dicas principais:

1) Utilize numeração para poder definir tons de cor e não superlativos. Por exemplo:

2) Nomeie os tokens dessa classe de acordo com a cor ou função muito específica. Ainda não é a hora de definir cores primárias ou secundárias.

Typography

Uma classe Typography será composta pelos tokens de texto que o seu time de design definiu, como a família da fonte, peso e altura do texto. Como na classe Colors, essa classe será a fonte da verdade em relação a tipografia da sua app.

Vou me repetir um pouco aqui, mas é importante: idealmente, você deve evitar utilizar tokens de tipografia que não estejam nesta classe, ou seja, não foram definidos pelo seu time de design. Se tiver uma tipografia nova, adicione-a nessa classe. Também retorno com a recomendação de evitar utilizar esses tokens diretamente nos seus widgets, mas sim, utilizar a camada de temas para isso, ok?

import 'package:flutter/material.dart';

class MyAppTypography {
  static const _baseFont = 'Open Sans';
  static const _titleFont = 'Roboto';

  static const _textStyle = TextStyle(fontFamily: _baseFont);
  static const _titleStyle = TextStyle(fontFamily: _titleFont);

  static final TextStyle txBody = _textStyle.copyWith(
    fontSize: 16,
    fontWeight: FontWeight.w400,
    height: 1.75,
    leadingDistribution: TextLeadingDistribution.even,
  );

  static final TextStyle txBodyBold = txBody.copyWith(
    fontWeight: FontWeight.w700,
  );

  static final TextStyle txButton = _textStyle.copyWith(
    fontSize: 14,
    fontWeight: FontWeight.w700,
    height: 1.425,
    leadingDistribution: TextLeadingDistribution.even,
  );

  static final TextStyle txButtonSmall = _textStyle.copyWith(
    fontSize: 12,
    fontWeight: FontWeight.w700,
    height: 1.5,
    leadingDistribution: TextLeadingDistribution.even,
  );

  static final TextStyle txBodySmall = _textStyle.copyWith(
    fontSize: 14,
    fontWeight: FontWeight.w400,
    height: 1.571,
    leadingDistribution: TextLeadingDistribution.even,
  );

  static final TextStyle txSubtitle = _titleStyle.copyWith(
    fontSize: 16,
    fontWeight: FontWeight.w600,
    height: 1.5,
    leadingDistribution: TextLeadingDistribution.even,
  );

  static final TextStyle txTitle1 = _titleStyle.copyWith(
    fontSize: 32,
    fontWeight: FontWeight.w700,
    height: 1.375,
    leadingDistribution: TextLeadingDistribution.even,
  );

  static final TextStyle txTitle2 = _titleStyle.copyWith(
    fontSize: 24,
    fontWeight: FontWeight.w700,
    height: 1.33333,
    leadingDistribution: TextLeadingDistribution.even,
  );

  static final TextStyle txTitle3 = _titleStyle.copyWith(
    fontSize: 22,
    fontWeight: FontWeight.w700,
    height: 1.363,
    leadingDistribution: TextLeadingDistribution.even,
  );

  static const TextStyle emojiBig = TextStyle(
    fontSize: 24,
    height: 1,
  );

  static const TextStyle emojiMedium = TextStyle(
    fontSize: 20,
    height: 1,
  );

  static const TextStyle emojiSmall = TextStyle(
    fontSize: 16,
    height: 1,
  );
}
Github: https://gist.github.com/gabrielaraujoz/ec50c7f7dee9d71c29e52f1c31e1cde5#file-newrevelobadge-dart

Vale notar:

  1. Com essa classe, temos um local centralizado onde podemos alterar a nossa tipografia de uma vez só - seja altura do texto, tamanho da fonte ou peso dela.
  2. É legal utilizarmos tokens de texto para emojis com height = 1, assim evitamos que o tamanho deles fique desproporcional ao texto que os esteja acompanhando.
  3. Note que, nos tokens de texto, não definimos as cores deles. Deixaremos este trabalho para o tema.

Ok, agora já temos classes específicas para os nossos tokens de cor e texto. Porém, isso ainda não atende a nossa necessidade de manutenibilidade e usabilidade pelas pessoas desenvolvedoras do nosso time, né? Quando temos contextos diferentes para a mesma cor, adicionamos uma cor nova para cada um dos seus usos ou utilizamos uma camada extra, que podemos inclusive estender e sobrescrever?

Para responder estas perguntas, vamos falar agora sobre a centralização desses tokens em um tema personalizado. Vem comigo!

Consolidando um tema personalizado de estilos e cores

O que faremos agora é criar uma classe de tema, que será a centralização dos tokens e também poderá aplicar mudanças a eles, criando tokens com funcionalidades específicas. Ela vai ser a camada extra que mencionei acima, permitindo uma personalização maior do uso de cores sem ficar com tokens duplicados. Acredite, seu time de design vai amar quando você tiver isso na sua app.

Vamos ver como podemos fazer com os tokens de cor:

Como você pode ver, agora temos cores com funções de acordo com o design, como as cores primárias e secundárias, cores de botão, cor de destaque (featuredColor) e cores de contraste para serem utilizadas em textos.

Falando em textos, vamos ver como ficam alguns estilos de texto para o nosso tema?

	TextStyle get txBody => MyAppTypography.txBody.copyWith(color: highContrastColor);

  TextStyle get txBodyMediumEmphasis => txBody.copyWith(color: mediumContrastColor);

  TextStyle get txBodyDisabled => txBody.copyWith(color: disabledTextColor);

  TextStyle get txBodyHighEmphasis => MyAppTypography.txBodyBold.copyWith(color: highContrastColor);

  TextStyle get txBodyHghEmphasisDisabled => MyAppTypography.txBodyBold.copyWith(color: disabledTextColor);

  TextStyle get txBodySmall => MyAppTypography.txBodySmall.copyWith(color: highContrastColor);

  TextStyle get txBodySmallMediumEmphasis => txBodySmall.copyWith(color: mediumContrastColor);

  TextStyle get txBodySmallDisabled => txBodySmall.copyWith(color: disabledTextColor);

  TextStyle get txSubtitle1 => MyAppTypography.txSubtitle.copyWith(color: highContrastColor);

  TextStyle get txSubtitle1MediumEmphasis => txSubtitle1.copyWith(color: mediumContrastColor);

  TextStyle get txTitle1 => MyAppTypography.txTitle1.copyWith(color: highContrastColor);

  TextStyle get txTitle2 => MyAppTypography.txTitle2.copyWith(color: highContrastColor);

  TextStyle get txTitle3 => MyAppTypography.txTitle3.copyWith(color: highContrastColor);

  TextStyle get elevatedButtonTextStyle => MyAppTypography.txButton;

  TextStyle get textButtonTextStyle => MyAppTypography.txButton.copyWith(color: textButtonColor);

  TextStyle get textButtonSmallTextStyle =>
      MyAppTypography.txButtonSmall.copyWith(color: textButtonColor);

  TextStyle get emojiBig => MyAppTypography.emojiBig;

  TextStyle get emojiMedium => MyAppTypography.emojiMedium;

  TextStyle get emojiSmall => MyAppTypography.emojiSmall;
Github: https://gist.github.com/gabrielaraujoz/ec50c7f7dee9d71c29e52f1c31e1cde5#file-newrevelobadge-dart

Aqui, perceba que temos algumas coisas interessantes, como o txBody normal com cores de contraste highContrast, mediumConstrast e lowContrast (disabledColor). Ainda com a mesma fonte, temos o txBody com highEmphasis, que utiliza a tipografia bold ao invés da normal.

Então, a classe final pode ser:

class GeneralAppTheme {
  final BuildContext context;

  const GeneralAppTheme(
    this.context,
  );

  /// Colors
  Color get primaryColor => MyAppColors.redBase;

  Color get primaryLight01 => MyAppColors.redLight01;

  Color get primaryLight02 => MyAppColors.redLight02;

  Color get primaryLight03 => MyAppColors.redLight03;

  Color get primaryDark01 => MyAppColors.redDark01;

  Color get primaryDark02 => MyAppColors.redDark02;

  Color get secondaryDark01 => MyAppColors.blueDark;

  Color get secondaryColor => MyAppColors.blue;

  Color get secondaryLight01 => MyAppColors.blueLight;

  Color get surfaceColor => MyAppColors.white;

  Color get backgroundColor => MyAppColors.grayLight03;

  Color get highContrastColor => MyAppColors.grayDark02;

  Color get mediumContrastColor => MyAppColors.grayDark01;

  Color get lowContrastColor => MyAppColors.grayLight01;

  Color get hintTextColor => lowContrastColor;

  Color get disabledTextColor => lowContrastColor;

  Color get disabledComponentColor => MyAppColors.grayLight03;

  Color get featuredColor => primaryColor;

  Color get loadingColor => primaryLight02;

  Color get errorColor => MyAppColors.error;

  Color get warningColor => MyAppColors.warning;

  Color get linkColor => MyAppColors.blue;

  Color get elevatedButtonColor => primaryColor;

  Color get elevatedButtonDisabledColor => primaryLight03;

  Color get containedButtonTextColor => MyAppColors.white;

  Color get containedButtonDisabledTextColor => lowContrastColor;

  Color get textButtonColor => primaryDark01;

  /// Typography
  TextStyle get txBody => MyAppTypography.txBody.copyWith(color: highContrastColor);

  TextStyle get txBodyMediumEmphasis => txBody.copyWith(color: mediumContrastColor);

  TextStyle get txBodyDisabled => txBody.copyWith(color: disabledTextColor);

  TextStyle get txBodyHe => MyAppTypography.txBodyBold.copyWith(color: highContrastColor);

  TextStyle get txBodyHeDisabled => MyAppTypography.txBodyBold.copyWith(color: disabledTextColor);

  TextStyle get txBodySmall => MyAppTypography.txBodySmall.copyWith(color: highContrastColor);

  TextStyle get txBodySmallMediumEmphasis => txBodySmall.copyWith(color: mediumContrastColor);

  TextStyle get txBodySmallDisabled => txBodySmall.copyWith(color: disabledTextColor);

  TextStyle get txSubtitle1 => MyAppTypography.txSubtitle.copyWith(color: highContrastColor);

  TextStyle get txSubtitle1MediumEmphasis => txSubtitle1.copyWith(color: mediumContrastColor);

  TextStyle get txTitle1 => MyAppTypography.txTitle1.copyWith(color: highContrastColor);

  TextStyle get txTitle2 => MyAppTypography.txTitle2.copyWith(color: highContrastColor);

  TextStyle get txTitle3 => MyAppTypography.txTitle3.copyWith(color: highContrastColor);

  TextStyle get elevatedButtonTextStyle => MyAppTypography.txButton;

  TextStyle get textButtonTextStyle => MyAppTypography.txButton.copyWith(color: textButtonColor);

  TextStyle get textButtonSmallTextStyle =>
      MyAppTypography.txButtonSmall.copyWith(color: textButtonColor);

  TextStyle get emojiBig => MyAppTypography.emojiBig;

  TextStyle get emojiMedium => MyAppTypography.emojiMedium;

  TextStyle get emojiSmall => MyAppTypography.emojiSmall;
}    
Github: https://gist.github.com/gabrielaraujoz/ec50c7f7dee9d71c29e52f1c31e1cde5#file-newrevelobadge-dart

Prontinho, agora você tem um tema para chamar de seu. Mas como vamos utilizá-lo de maneira eficiente? Te explico no próximo tópico.

Widget de tema personalizado e ThemeWrapper (avançado)

Garantindo a presença do tema no context com um widget de tema personalizado

Vamos utilizar a classe InheritedWidget do Flutter para podermos manter as informações do nosso tema disponíveis no contexto certo da Widget Tree. Com isso, poderemos acessar os tokens que definimos no tema de maneira mais fácil, sem precisarmos instanciar a classe todas as vezes.

Nossa classe de tema personalizado dependerá do tema que definimos na última seção (GeneralAppTheme) e, para podermos garantir que o contexto será definido corretamente, também adicionaremos um parâmetro de tipo Widget chamado child por conta do InheritedWidget.

Nossa classe ficará assim:

class MyCustomAppTheme extends InheritedWidget {
  final GeneralAppTheme theme;
  final Widget child;

  const MyCustomAppTheme({
    required this.theme,
    required this.child,
  }) : super(child: child);

  @override
  bool updateShouldNotify(covariant MyCustomAppTheme oldWidget) {
    return oldWidget.theme != theme;
  }

  static GeneralAppTheme of(BuildContext context) =>
      context
          .dependOnInheritedWidgetOfExactType<MyCustomAppTheme>(aspect: MyCustomAppTheme)
          ?.theme ??
      GeneralAppTheme(context);
}
Github: https://gist.github.com/gabrielaraujoz/ec50c7f7dee9d71c29e52f1c31e1cde5#file-newrevelobadge-dart

A função estática of  servirá para acessarmos o nosso tema mais facilmente e o updateShouldNotify fará a árvore ser reconstruída caso o tema seja alterado.

Para utilizar esse tema, é simples:

ThemeWrapper

Já está legal, mas vamos deixar ainda melhor? Vamos fazer um Wrapper para esse tema, ele vai facilitar sua utilização e evitar que você esqueça de utilizar o Builder para ter o contexto correto.

A classe (na verdade, Widget) é bem simples:

A sua utilização também é simples! Olha só:

Temas para wigdets específicos (avançado)

Agora digamos que você não quer ter um tema que possua cada uso de cores ou estilos diferente para cada widget, por exemplo: se eu tenho um botão azul, terei uma blueButtonColor e se eu tenho um botão vermelho terei mais um token no tema, como uma redButtonColor. Com apps maiores, gerenciar e dar manutenção em todos esses temas dentro de widgets começa a ficar bem complexo e, como devs, não é isso que queremos.

Vamos então criar dois temas de botão, um azul e um vermelho, que modificarão o token elevatedButtonColor do nosso tema principal.

Estes temas podem ser utilizados da mesma maneira do GeneralAppTheme:

Fácil, não? :)

Conclusão

É isso aí! Agora sim você está craque de temas e conseguirá organizar seus tokens, temas personalizados para widgets específicos e seus projetos como um todo de uma maneira muito mais replicável e manutenível.

Esta série de artigos foi um resumo mais detalhado dos meus aprendizados sobre temas no Flutter dentro da Revelo. Obrigado por me acompanhar nessa jornada! Fica aqui também o meu agradecimento ao Cesar Castro e ao Douglas Iacovelli por terem me ensinado MUITO do que eu mostrei aqui para você!

Espero que tenha conseguido te ajudar consolidar seu conhecimento sobre 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!

💡
As opiniões e comentários expressos neste artigo são de propriedade exclusiva de seu autor e não representam necessariamente o ponto de vista da Revelo.

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.