Tokens de design no Flutter

Tokens de design no Flutter

Já parou para pensar como é possível deixar a sua aplicação Flutter mais bonita e do seu jeitinho, mas também poder ter um padrão que será utilizado em nela toda? Quais são os principais elementos que precisamos aprender para podermos fazer essa personalização? Onde vivem, o que fazem, o que comem?

Neste artigo, vamos aprender o que são tokens de cor e tipografia (texto) e como eles funcionam no Flutter.

O que é um token de design

Tokens são pequenas partes de um design que compõe a personalidade da sua aplicação. Cada cor e cada estilo de texto podem ser considerados tokens. Os tokens de design da sua aplicação permitirão que você estilize todos os componentes dela e, por isso, é bastante importante entender todo o seu potencial e como trabalhar com eles.

Tokens de design permitem flexibilidade e consistência dentro de uma aplicação, pois permitem que times de design definam qual é o papel da cor em uma UI ao invés de um valor de cor.

Além disso, eles agem como uma ponte entre a função de um elemento e a cor escolhida para aquela determinada função. Por exemplo, é possível termos tokens de cor primária, secundária, de superfície, de fundo, de botão, e também termos tokens de texto de título, de corpo com ênfases diferentes. O céu é o limite por aqui.

O Flutter fornece algumas opções em relação a tokens. Para cores, ele permite que você crie o seu próprio jogo de cores ou que você utilize cores padrão do Material. Já para tipografia, ele permite que você defina seus tamanhos, cores, fontes, etc.

Um exemplo rápido de cores e textos com funções:

//feedback colors
  static const success = Color(0xFF59C559);
  static const warning = Color(0xFFEFCC43);
  static const error = Color(0xFFE0493E);

// text styles
static final TextStyle captionText = TextStyle(
		fontFamily: 'open_sans',
    fontSize: 12,
    fontWeight: FontWeight.w400,
    height: 1.5,
    leadingDistribution: TextLeadingDistribution.even,
  );

  static final TextStyle captionTextBold = captionText.copyWith(
    fontWeight: FontWeight.w700,
  );

  static final TextStyle subtitleText = TextStyle(
		fontFamily: 'open_sans',
    fontSize: 16,
    fontWeight: FontWeight.w600,
    height: 1.5,
    leadingDistribution: TextLeadingDistribution.even,
  );

  static final TextStyle titleTextLarge = TextStyle(
		fontFamily: 'open_sans',
    fontSize: 32,
    fontWeight: FontWeight.w700,
    height: 1.375,
    leadingDistribution: TextLeadingDistribution.even,
  );

  static final TextStyle titleTextMedium = TextStyle(
		fontFamily: 'open_sans',
    fontSize: 22,
    fontWeight: FontWeight.w700,
    height: 1.363,
    leadingDistribution: TextLeadingDistribution.even,
  );

  static final TextStyle titleTextSmall = TextStyle(
		fontFamily: 'open_sans',
    fontSize: 14,
    fontWeight: FontWeight.w700,
    height: 1.571,
    leadingDistribution: TextLeadingDistribution.even,
  );


Tokens de Cor

As cores no Flutter podem ser definidas de diversas maneiras, a depender do tipo de cor que você ou seu time de design utilizaram quando criaram as telas da sua app.

Alguns exemplos de tokens de cor que o Flutter entende:

Hexadecimal:
	8 dígitos - 0xFF42A5F5
	6 dígitos - 0x42A5F5

ARGB (alpha ou transparência, vermelho, verde, azul):
	com Hex - (0xFF, 0x42, 0xA5, 0xF5)
	com notação RGB - (255, 66, 165, 245)

RGBO (vermelho, verde, azul, opacidade): (66, 165, 245, 1.0)
	// note que a opacidade, diferente do alpha do ARGB, varia de 0.0 a 1.0

A classe Color

O Flutter tem uma classe dedicada para cores, chamada (olha só!) Color. A documentação dela está nesse link: Flutter Color.

Essa classe é como, no fundo, o Flutter entende todas as cores que você pode utilizar na sua app. Ela é formada por um valor de cor imutável de 32 bits, no formato ARGB. Você consegue construir cores com ela a partir de alguns construtores, a depender dos tokens que você tiver:

Color c = const Color(0xFF42A5F5); - Hexadecimal
Color c = const Color.fromARGB(0xFF, 0x42, 0xA5, 0xF5); - ARGB Hex
Color c = const Color.fromARGB(255, 66, 165, 245); - ARGB
Color c = const Color.fromRGBO(66, 165, 245, 1.0); - RGBO

Um detalhe importante é que, quando você utilizar valores em hexadecimal, não pode se esquecer de colocar, no início, o valor do alpha ou transparência.

Color c1 = const Color(0x42A5F5); // cor totalmente transparente (invisível)
Color c2 = const Color(0xFF42A5F5); // cor totalmente opaca (visível)

Bônus: quer criar uma cor nova com base em uma outra já existente? Você consegue fazer isso com os seguintes métodos:

Color baseColor = const Color.fromARGB(255, 66, 165, 245);

Color transparentColor =  baseColor.withAlpha(0);
Color redderColor = baseColor.withRed(160);
Color greenerColor = baseColor.withGreen(200);
Color lessBlueColor = baseColor.withBlue(50);
Color baseColor50 = baseColor.withOpacity(0.5);

O exemplo acima, ilustrado:

Caso queira o código para fazer seus próprios testes, aqui está:

// Copyright 2019 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

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

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  static const Color baseColor = Color.fromARGB(255, 66, 165, 245);

  Color transparentColor = baseColor.withAlpha(0);
  Color redderColor = baseColor.withRed(160);
  Color greenerColor = baseColor.withGreen(200);
  Color lessBlueColor = baseColor.withBlue(50);
  Color baseColor50 = baseColor.withOpacity(0.5);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Expanded(
              child: Container(
                width: double.maxFinite,
                alignment: Alignment.center,
                child: const Text('normal background'),
              ),
            ),
            Expanded(
              child: Container(
                width: double.maxFinite,
                alignment: Alignment.center,
                color: baseColor,
                child: Text('baseColor: $baseColor'),
              ),
            ),
            Expanded(
              child: Container(
                width: double.maxFinite,
                alignment: Alignment.center,
                color: baseColor50,
                child: Text('baseColor50: $baseColor50'),
              ),
            ),
            Expanded(
              child: Container(
                width: double.maxFinite,
                alignment: Alignment.center,
                color: transparentColor,
                child: Text('transparentColor: $transparentColor'),
              ),
            ),
            Expanded(
              child: Container(
                width: double.maxFinite,
                alignment: Alignment.center,
                color: redderColor,
                child: Text('redderColor: $redderColor'),
              ),
            ),
            Expanded(
              child: Container(
                width: double.maxFinite,
                alignment: Alignment.center,
                color: greenerColor,
                child: Text('greenerColor: $greenerColor'),
              ),
            ),
            Expanded(
              child: Container(
                width: double.maxFinite,
                alignment: Alignment.center,
                color: lessBlueColor,
                child: Text('lessBlueColor: $lessBlueColor'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

A classe Colors

O Flutter tem mais uma classe dedicada para cores, a Colors. A documentação dela está nesse link: Flutter Colors.

A Colors já é uma implementação do Material, com as cores pré-definidas com diferentes tonalidades. Essas tonalidades já são pensadas para servirem uma função específica, facilitando o uso do Material. Podemos definir a classe Colors como uma paleta de cores a partir de uma cor principal.

Alguns detalhes de implementação importantes para ressaltar em relação a essa classe:

// Para selecionar uma cor específica dentro de uma paleta de cores
Color specificSwatch = Colors.amber[400]! //seleciona uma cor mais clara que a amber normal

// Cada paleta de cores tem uma cor constante e pode ser usada diretamente
Container(
	color: Colors.amber, // isso é a mesma coisa que utilizar Colors.amber[500] ou Colors.amber.shade500
);

A maioria das cores da Colors tem uma variação entre 100 e 900 dentro da paleta. Quanto menor o número, a cor fica clara e, quanto maior o número, a cor fica mais escura.

As cores de acento (ou acentuação) também são derivadas da cor principal, obedecendo as mesmas regras de implementação.

// Para selecionar uma cor específica dentro de uma cor de acentuação
Color specificSwatch = Colors.amberAccent[400]! //seleciona uma cor mais escura que a amberAccent

// A cor de acentuação também tem uma cor padrão constante
Container(
	color: Colors.amberAccent,
);

A classe Colors também possui variantes de preto e branco.

Essas cores são identificadas de acordo com a sua transparência. Quanto menor o número, mais transparente a cor é e, por isso, mais difícil de ser visualizada. É recomendado que cores mais transparentes sejam evitadas a não ser que tenham um uso específico e sutil.

Finalizando, também temos a “cor” transparente, com o construtor:

Color transparent = Colors.transparent;

Não conseguiremos mostrá-la porque, como você já deve ter imaginado, ela é transparente! ;)

Tokens de tipografia

A tipografia é a arte e técnica de organizar textos com o objetivo de fazer a linguagem escrita legível, compreensível e atrativa onde for mostrada. Conseguimos atingir alguns objetivos muito legais com uso de tipografia como o reconhecimento de marcas pelo usuário (Brand Recognition).

Quando falamos de tokens de tipografia, temos várias escolhas possíveis: a fonte, o tamanho da fonte, o peso da fonte (em termo mais simples, a “grossura” dela), o espaçamento entre letras e entre linhas, a cor do texto.

No Flutter, as fontes padrão são:

Podemos alterar os parâmetros dessas fontes no Flutter utilizando uma classe chamada TextStyle. Vamos lá?

A classe TextStyle

A classe TextStyle é uma classe de estilo imutável que descreve como o Flutter deve formatar e pintar um texto.

const Text(
  'Este texto deveria estar em itálico',
  style: TextStyle(fontStyle: FontStyle.italic),
);
const Text(
  'Este texto deveria estar em negrito',
  style: TextStyle(fontWeight: FontWeight.bold),
);

Vamos passar por cada um dos atributos mais utilizados desta classe para que você possa ter um entendimento mais completo.

Color? backgroundColor

O atributo backGroundColor define a cor de fundo do texto que possuir este estilo a partir de uma color. Por exemplo:

Text(
	'normal background com baseColor de background',
	style: TextStyle(
	  backgroundColor: baseColor,
	),
);

double? fontSize

O atributo fontSize se refere ao tamanho do texto, em logical pixels. Caso queira saber mais sobre logical pixels, aqui está um link da documentação: devicePixelRatio.

double? height

O atributo height se refere ao espaço vertical que a fonte tomará, com base no tamanho do texto (fontSize). Em outras palavras, ele se compara ao line-height no CSS. É importante manter em mente que este valor multiplicará o fontSize, então se utilizarmos height = 3.0 teremos uma altura de linha 3 vezes maior que o tamanho da fonte.

Caso o height não seja especificado, será utilizada a altura padrão de linha da fonte. Diretamente da documentação do Flutter, vamos ver alguns exemplos:

Fonte: Flutter API Doc

De maneira mais visual, temos:

FontWeight? fontWeight

O atributo fontWeight se refere ao peso da fonte, ou seja, a espessura da font quando ela é renderizada. Ela pode ser utilizada para aplicarmos negrito em uma fonte, por exemplo.

Um detalhe importante aqui é sempre se lembrar de garantir que você tenha a fonte com o fontWeight especificado disponível na sua aplicação. Se a fonte não estiver disponível, o texto não será renderizado com a espessura especificada, mas sim com a padrão da fonte.

Esse atributo pode ser especificado desde FontWeight.w100 até FontWeight.w900.

## exemplos de import de fontes com espessuras diferentes:

flutter:	
	fonts:
    - family: Work Sans
      fonts:
        - asset: fonts/WorkSans-Black.ttf
        - asset: fonts/WorkSans-Bold.ttf
        - asset: fonts/WorkSans-Medium.ttf
        - asset: fonts/WorkSans-Regular.ttf
        - asset: fonts/WorkSans-SemiBold.ttf
		- family: Raleway
      fonts:
        - asset: fonts/Raleway-Regular.ttf
        - asset: fonts/Raleway-Medium.ttf
          weight: 500 ## peso especificado
        - asset: assets/fonts/Raleway-SemiBold.ttf
          weight: 600

FontStyle? fontStyle

O atributo de estilo da fonte vai definir se temos uma fonte com estilo normal ou itálico. Conseguimos definir este atributo utilizando a classe FontStyle.

String? fontFamily

O atributo fontFamily define qual será a fonte utilizada neste estilo de texto. Uma aplicação pode ser várias fontes diferentes para contextos diferentes. É importante garantir que a fonte esteja corretamente importada no pubspec.yaml do seu projeto, como mostrado acima.

List<String>? fontFamilyFallback

O atributo fontFamilyFallback será utilizado caso a fontFamily explicitada não dê suporte a um caractere específico. Por exemplo: se a sua aplicação precisa de textos em inglês e em japonês, é mais seguro que você sempre tenha o fallback de uma fonte que suporte os caracteres japoneses ou kanjis.

É interessante pensar em fontes para emojis também, caso queira que emojis sejam representados da mesma maneira em todas as plataformas.

const TextStyle(
  fontFamily: 'Roboto',
  fontFamilyFallback: <String>[
    'Noto Sans CJK SC',
		'Noto Sans Japanese',
    'Noto Color Emoji',
  ],
)

double? letterSpacing

O atributo letterSpacing é o espaçamento entre caracteres, em logical pixels.

double? wordSpacing

O atributo wordSpacing é o espaçamento entre as palavras, em logical pixels.

TextDecoration? decoration

O atributo decoration se refere à decoração do texto, como sublinhado, tachado, etc. Podemos definir este atributo com a classe TextDecoration.

Container(
  width: double.maxFinite,
  padding: const EdgeInsets.all(16),
  alignment: Alignment.center,
  child: const Text(
    'aqui os exemplos de decoration',
    style: TextStyle(
			// TextDecoration.underline, TextDecoration.overline, 
			// TextDecoration.lineThrough ou uma combinação entre eles 
      decoration: TextDecoration.underline,
    ),
  ),
),

Color? decorationColor

O atributo decorationColor se refere à cor da decoration. Aqui você pode passar qualquer cor como parâmetro e o sublinhado, tachado ou sobrelinhado terá essa cor.

TextDecorationStyle? decorationStyle

O atributo decorationStyle permite uma personalização ainda maior da decoration. Ela pode ser sólida, tracejada, pontuada, ondulada ou até mesmo dupla.

double? decorationThickness

O atributo decorationThickness configura a grossura da decoration, em logical pixels.

TextOverflow? overflow

O atributo overflow permite personalizar como o Flutter lida com a quebra do texto quando o ele não cabe na tela dentro da quantidade de linhas permitida. Seguem as possibilidades que temos:

Código fonte

Não poderia te deixar sem um código fonte maneiro para você poder brincar e descobrir ainda mais coisas sobre essas implementações né? O link está aqui para você: Flutter TextStyle Demo :)

Pronto!

Ufa, quem diria que falar de design tokens daria tanta coisa, né? Mas agora você pode se considerar empoderado em relação a como personalizar os seus tokens de design de maneira mais profunda.

Espero que tenha conseguido te ajudar a incorporar alguns conceitos importantes de cores, textos e tokens em geral. 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.Jueves, 5 de enero