Cómo utilizar temas en Flutter - Parte 2

Cómo utilizar temas en Flutter - Parte 2

En la primera parte de esta serie sobre cómo usar temas en Flutter, vimos algunos conceptos básicos de temas, aprendimos conceptos como surface y background y usamos ThemeData para personalizar nuestras aplicaciones.

Este uso más básico puede ser realmente genial para proyectos personales o pequeños que no tienen muchos desarrolladores involucrados en el desarrollo de la interfaz de usuario de la aplicación.

Cuando la aplicación empieza a ganar volumen de usuarios, código y también relevancia en las tiendas, se vuelve cada vez más importante pensar en cómo hacer que sea más fácil de mantener, que sea entendida por todos los involucrados en el proyecto y, también, cómo evitar que diferentes tokens entren en juego. siendo utilizados para las mismas funciones o contextos.

Pensándolo bien, ¿cómo podemos hacerlo si tenemos una aplicación grande o un sistema de diseño complejo y necesitamos asegurarnos de que todos los involucrados en el proyecto usen los mismos colores y estilos de texto?

¡Ven conmigo y hoy vamos a hablar sobre temas personalizados de una manera más avanzada! Are you ready? Let's go!

Un pequeño disclaimer

Debido a la complejidad del tema, utilizaré como base para este artículo las prácticas aplicadas en las aplicaciones Flutter de Revelo y las lecciones aprendidas a lo largo del tiempo.

En Revelo, organizamos nuestros temas en torno a dos clases de fichas: Colors y Typography. Esto nos ayudará a tener estos tokens (tanto de color como de texto) centralizados en un solo lugar, haciéndolos más fáciles de mantener. ¡Ya hemos pasado por dos rebrandings en nuestra aplicación y estas clases ayudaron mucho!

Colors


Una clase Colors estará compuesta por tokens de color definidos por tu equipo de diseño y serán la fuente de la verdad con respecto a ellos. Lo ideal es evitar el uso de colores que no sean de esta clase o que parezcan indefinidos por su tiempo de concepción. También recomiendo como buena práctica evitar el uso de estos tokens directamente en tus widgets, pero use una capa de temas para eso, ¿de acuerdo?

Aquí hay dos consejos principales:

1) Usa numeración para definir matices de color y no superlativos. Por ejemplo:

2) Nombra los tokens de esta clase según un color o función muy específica. Todavía no es el momento de definir los colores primarios o secundarios.

Typography

Una clase de Typography estará compuesta por los tokens de texto que haya definido su equipo de diseño, como la familia de fuentes, el peso y la altura del texto. Al igual que la clase Colors, esta clase será la fuente de la verdad con respecto a la tipografía de tu aplicación.

Voy a repetir un poco aquí, pero es importante: idealmente, deberías evitar usar tokens tipográficos que no estén en esta clase, es decir, que no hayan sido definidos por tu equipo de diseño. Si tienes una nueva tipografía, agrégala a esta clase.

También vuelvo con la recomendación de evitar usar estos tokens directamente en tus widgets, pero en su lugar, usa la capa de temas para eso, ¿de acuerdo?

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

Es bueno mencionar:

  1. Con esta clase, tenemos un lugar centralizado donde podemos cambiar nuestra tipografía de una sola vez, ya sea la altura del texto, el tamaño de la fuente o el peso de la fuente.
  2. Es bueno usar tokens de texto para emojis con altura = 1, así evitamos que su tamaño sea desproporcionado con respecto al texto que los acompaña.
  3. Ten en cuenta que en los tokens de texto no definimos sus colores. Dejaremos este trabajo por el tema.

Ok, ahora tenemos clases específicas para nuestros tokens de color y texto. Sin embargo, esto todavía no satisface nuestra necesidad de mantenimiento y usabilidad por parte de los desarrolladores de nuestro equipo, ¿verdad? Cuando tenemos diferentes contextos para un mismo color, ¿agregamos un nuevo color para cada uno de sus usos o usamos una capa extra, que incluso podemos extender y sobrescribir?

Para responder a estas preguntas, hablemos ahora sobre la centralización de estos tokens en un tema personalizado. ¡Ven conmigo!

Consolidando un tema personalizado de estilos y colores


Lo que haremos ahora es crear una clase de tema que centralizará los tokens. También podrás aplicarles cambios, creando tokens con una funcionalidad específica. Será la capa adicional mencionada anteriormente la que permitirá una mayor personalización en el uso de colores sin quedarse con tokens duplicados.

Confía en mí: a tu equipo de diseño le encantará tener esto en tu aplicación.

Veamos qué podemos hacer con las fichas de color:

Como puedes ver, ahora tenemos colores con funciones de acuerdo al diseño, como colores primarios y secundarios, colores de botones, color de acento (featuredColor) y colores de contraste para ser usados ​​en textos.

Hablando de textos, veamos cómo se ven algunos estilos de texto para nuestro 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

Observa que tenemos algunos detalles interesantes como el txBody normal con colores de contraste highContrast, mediumContrast y lowContrast (disabledColor). Aún con la misma fuente, tenemos txBody con highEmphasis, que usa tipografía en negrita en lugar de normal.

Así que la clase final podría 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

Eso es todo, ahora tienes un tema para considerarlo tuyo. Pero, ¿cómo vamos a usarlo de manera eficiente? Lo explico a continuación.

Widget de tema personalizado y ThemeWrapper (avanzado)


Garantizar la presencia del tema en el context con un widget de tema personalizado


Usemos la clase InheritedWidget de Flutter para que podamos mantener la información de nuestro tema disponible en el contexto correcto del Widget Tree.

Con esto, podremos acceder a los tokens que definimos en el tema de una manera más fácil, sin tener que instanciar la clase cada vez.

Nuestra clase de tema personalizado dependerá del tema que definimos en la última sección (GeneralAppTheme) y, para asegurarnos de que el contexto se establezca correctamente, también agregaremos un parámetro de tipo de widget llamado child en nombre del InheritedWidget.

Nuestra clase se verá así:

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

La función estática of servirá para acceder más fácilmente a nuestro tema y el updateShouldNotify hará que el árbol se reconstruya en caso de que el tema cambie.

Usar este tema es simple:

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:

¡Su uso también es sencillo! Solo mira:

Temas para wigdets específicos (avanzado)


Ahora supongamos que no deseas un tema que tenga colores o estilos diferentes para cada widget, por ejemplo: si tengo un botón azul, tendré un blueButtonColor y si tengo un botón rojo, tendré uno más. token en el tema como redButtonColor. Con aplicaciones más grandes, administrar y mantener todos estos temas dentro de los widgets comienza a ser bastante complejo y, como desarrolladores, eso no es lo que queremos.

Luego, creemos dos temas de botones, uno azul y otro rojo, que modificarán el token de elevatedButtonColor de nuestro tema principal.

Estos temas pueden utilizarse de la misma manera que el GeneralAppTheme:

Fácil, ¿verdad?

Conclusión


¡Listo! Ahora eres experto/a en temas y podrás organizar tus tokens, temas personalizados para widgets específicos y tus proyectos de manera mucho más replicable y mantenible.

Esta serie de artículos fue un resumen detallado de mis aprendizajes sobre temas de Flutter dentro de Revelo. ¡Gracias por acompañarme en este viaje!

Agradezco mucho a Cesar Castro y a Douglas Iacovelli por enseñarme MUCHO de lo que les mostré aquí.

Espero haberte ayudado a consolidar tu conocimiento sobre cómo usar temas en Flutter. Cuéntame qué te parece y, si tienes alguna duda o sugerencia, contáctame en LinkedIn o a mi e-mail.

¡Gracias!

⚠️
Las opiniones y comentarios emitidos en este artículo son propiedad única de su autor y no necesariamente representan el punto de vista de Revelo.

Revelo Content Network da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.