Cómo utilizar temas en Flutter - Parte 1

Cómo utilizar temas en Flutter - Parte 1

La tematización o estilización son nombres genéricos para el acto de hacer una aplicación atractiva, atrayente para el usuario y que tenga sentido con la marca de la empresa o lo que necesita mostrar. Es lo que hace que su aplicación sea hermosa y logra interferir incluso con la accesibilidad para el usuario final.

Flutter tiene por defecto un tema o estilo que se basa en el color azul. Cualquiera que haya hecho un tutorial de Flutter ha visto esa aplicación azul ejecutándose, ¿verdad?

Captura de pantalla del primer tutorial de Flutter: “Write your first app”. Disponible en el link: https://docs.flutter.dev/get-started/codelab

Pero, ¿qué hacer cuando queremos personalizar nuestro diseño en su conjunto?
En esta primera parte, vamos a ver juntos cómo usar temas más genéricos de Flutter para que puedas personalizar tu aplicación con tu personalidad. Además, aprenderemos dos conceptos importantes: concepto de fondo y concepto de superficie.

En la segunda parte, aprenderá cómo crear temas específicos para cada widget o escenario que tenga en su aplicación, asegurando la singularidad en el desarrollo y acercándose un paso más a tener un sistema de diseño pequeño.

¿Qué es un tema?

Un tema no es más que un conjunto de colores y tipografías que permiten ajustar y mejorar varias propiedades visuales en una aplicación, como colores de fuente, colores de fondo y superficie, colores específicos de elementos de la interfaz de usuario, etc.

También podríamos considerar los diferentes conjuntos de Widgets, Cupertino y Material, como extensiones de estos temas. Pero ese tema será para la próxima, ¿OK?

En Flutter, es fundamental personalizar su aplicación para que se vea y transmita los mensajes correctos. El uso de temas proporciona relaciones de jerarquía, flujo y estructura de toda la interfaz de usuario, además de ayudar a su usuario a participar más e incluso a adquirir nuevos usuarios.

Flutter tiene un laboratorio de código enfocado en cómo hacer que tu aplicación pase de aburrida a hermosa, en caso de que estés interesado. La referencia está aquí: Haz que tu aplicación Flutter sea hermosa, no aburrida.

Conceptos de background y surface

En mi experiencia como desarrollador, ya he sufrido lo suficiente como para comprender los conceptos de background y surface. Esto se debe a que, en Flutter, su árbol de widgets se construye sobre una base (generalmente un Scaffold) y todo lo que contiene está encima (foreshadowing).

Antes de comenzar a resolver el problema, primero comprendamos qué resuelve.

Cuando observas el widget a continuación, ¿cuál crees que es el color de fondo?

Te daré una pista, aquí está el código para este 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,
                    ),
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

Puedes pensar que es el color naranja porque es el color de la tarjeta, que sirve como fondo para los widgets de texto, ¿verdad?

Pero ahora mira esto:

Veamos el código nuevamente:

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',
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

¿Y ahora qué? Tenemos el color de fondo de la Card, de las demás dentro de ella y también el color de fondo de la pantalla. ¿Cómo podemos identificar cuál es cuál? Para ello, es importante entender los conceptos de background y surface.

Ah, una nota importante: aprendimos y definimos estos dos conceptos en Revelo, junto con nuestro equipo de diseño de aplicaciones, ¡y funcionó bastante bien!

Todavía no he encontrado ninguna documentación más profunda que comente sobre los árboles de widgets de Flutter. No dudes en sugerirme otras fuentes, ¿de acuerdo? ¡Al final del texto tendrás acceso a mis contactos!

Concepto de background

Definimos que, para facilitar nuestras referencias, el background es la capa más baja de una interfaz de usuario. En el caso de Flutter, el background siempre será el color de scaffold de la pantalla. Volviendo a los ejemplos anteriores, la respuesta sobre los colores de fondo sería: el blanco detrás de todos los widgets.

Ilustro para una mejor comprensión:

En este ejemplo, todo lo que hemos hecho es aplicar un color de fondo al scaffold.

Sí, fue mucho más fácil combinar la definición con lo que ya usa Flutter, ¿verdad?

Concepto de surface

Con el concepto de background mejor definido, ahora es más fácil explicar el concepto de surface. Una surface puede ser cualquier superficie que esté antes del elemento más alto del árbol.

Imagina que en el árbol, la capa más profunda será el comienzo del mismo, el scaffold. Piensa en ello como la raíz, que será el fondo y todo lo que esté encima será una superficie. Pensemos en el ejemplo de la Card con otras cards adentro:

// 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.
					                ),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

¿Entiendes? Termina siendo simple, ¿verdad? Esa es la intención: ser simple y fácil de entender y aplicar.

Estos conceptos pueden ayudarte mucho a la hora de pensar en cómo implementar un sistema de diseño temático en tu aplicación, ¡confía en mí!

Utilizar ThemeData para personalizar tu aplicación Flutter

Flutter usa MaterialApp como base predeterminada en sus aplicaciones, que a su vez usa la clase ThemeData para personalizar el tema (parámetro de tema de MaterialApp) de la aplicación.

Recomiendo aquí nuevamente que hagas el codelab de Flutter sobre temas: Haz que tu aplicación Flutter sea hermosa, no aburrida.

La clase ThemeData

La clase ThemeData permite no solo la personalización del tema predeterminado de MaterialApp, como se mencionó anteriormente, sino también árboles de widgets específicos dentro de la aplicación. Podemos lograr estos resultados de varias maneras. ¿Vamos?

Cómo utilizar ThemeData

Para usar ThemeData, tenemos dos formas:

Directamente dentro del parámetro theme de la aplicación Material:

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 un árbol específico, rodeado por un Widget llamado 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 habrás notado, el primer fragmento define el tema general de la aplicación y el segundo se puede usar dentro de una parte específica de la misma, como una página o un componente.

Para poder acceder a un parámetro de tema, usamos el getter Theme.of(context) para acceder al tema del contexto actual y encontrar el parámetro correcto.

Constructores de ThemeData

Tenemos 5 constructores por defecto de la clase ThemeData, que facilitan la creación de nuevos temas:

ThemeData(): Es la factory más extensa de ThemeData. Permite definir campos específicos que sean necesarios, completando los demás campos según la temática general del Material. Con él, podemos definir temas específicos para algunos Widgets que son necesarios para la aplicación, como AppBarTheme, SnackBarThemeData, ProgressIndicatorThemeData, ButtonThemeData, CardTheme, DialogTheme, ElevatedButtonThemeData, etc.

ThemeData.from(): ThemeData.from() es una fáctory muy poderosa. Permite crear un ThemeData completo a partir de dos piezas de información: el colorScheme y el textTheme. Para conocer más sobre esta información, te recomiendo visitar su documentación: ColorScheme DocumentationTextTheme Documentation. Como ejemplo de aplicación sigue:

// 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 es el tema predeterminado con Colors.blue como color principal.

ThemeData.dark(): Este es un tema predeterminado más oscuro con Colors.grey[900] como color principal y Colors.tealAccent como color secundario en ColorScheme.

ThemeData.raw(): Este constructor es una constante y deberá definir todos los campos ThemeData para poder usarlo. Particularmente, nunca he visto una aplicación que use este constructor, ya que sería mucho trabajo definir todos los temas posibles que Material necesita para un tema. La principal diferencia de este raw() con el ThemeData normal es que se requieren los campos que aceptan valores NULL en ThemeData.

Aplicar temas en sub-árboles o componentes específicos

Ya hemos aprendido mucho, pero hay un punto importante adicional acerca de cuándo usamos el widget de tema que necesito contarte. ¡Juro que ya casi termino!

Pensemos en una aplicación que tiene definido el siguiente tema en su raíz, en MaterialApp:

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,
        ),
      );
    },
  ),
),

Ya hemos aprendido mucho, pero hay un punto más importante acerca de cuándo usamos el widget de tema que necesito contarte. Te juro que se acabó, ¿de acuerdo?

Pensemos en una aplicación que tiene definido el siguiente tema en su raíz, en MaterialApp:

Conclusión de código fuente

¡Bueno, eso es todo! ¡ThemeData y sus aplicaciones completan la primera parte de esta serie sobre temas! Si te resultó molesto tener que usar un Builder cada vez que usas un tema nuevo, no te preocupes: en la siguiente parte, veremos cómo personalizar nuestros temas aún más y eliminar algunos de estos obstáculos. Estoy emocionado, ¿y tú? ¡Vamos!

El código fuente utilizado para las imágenes y los códigos de esta parte están disponibles en el enlace: Flutter Theme Demo - Part 1.

Espero haberte ayudado a incorporar algunos conceptos importantes sobre qué es un background, qué es una surface y cómo usar temas en Flutter. Dime lo que piensas y, si tienes alguna pregunta o sugerencia, puedes buscarme en LinkedIn o al correo electrónico, ¿de acuerdo? ¡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.