Flutter: criando uma biblioteca de componentes

Moacir Zimerman Junior
Senior Sistemas
Published in
5 min readNov 21, 2023

Frequentemente as empresas que possuem vários produtos criam regras de design e garantem que estes produtos tenham uma mesma identidade. Criam componentes que serão reutilizados, definem regras de posicionamento, espaçamento, cores, etc. Chamamos isso de design system. Muitas vezes disponibilizam estes design systems para a comunidade que constroem diversos outros produtos com esta mesma identidade visual. Talvez você já tenha ouvido falar do Material Design da Google, Diretrizes da Interface Humana da Apple, Fluent Design da Microsoft ou o Carbon criado pela IBM. Existem também design systems criados pela própria comunidade para a comunidade. São várias as opções.

A empresa onde trabalho precisou criar um design system para suas novas aplicações móveis. A ideia era criar uma experiência unificada em todos os produtos. A equipe de design então definiu a nova experiência, com os componentes, as regras e os padrões. Mas precisávamos criar uma biblioteca de componentes que permitissem que os times de desenvolvimento pudessem colocar tudo isso em prática de uma maneira facilitada. A ideia era reaproveitar componentes previamente desenvolvidos nos aplicativos. E foi aí que entrei na brincadeira.

Para contextualizar o nosso cenário, todos os aplicativos seriam escritos utilizando Flutter e não usaríamos bibliotecas de terceiro para construir os componentes.

Meu primeiro passo foi definir como os componentes seriam escritos. Usar herança para, a partir dos componentes já existentes no Flutter, personalizá-los ou composição, criando os componentes a partir da união de outros componentes mais simples? Como o Flutter trabalha melhor com composição, visto que tudo que vemos é uma grande árvore de Widgets, esta foi a abordagem que seria usada na nossa biblioteca.

Uma vez definida a forma como os componentes seriam criados, precisávamos construir os componentes propriamente ditos. Iniciar com os componentes mais simples é a melhor forma de validar o nosso projeto. Então surgiram os botões, cards e campos de texto. Tudo funcional, publicado e já podendo ser utilizado nos outros projetos. Mas tinha mais um requisito que eu precisava atender. A personalização de temas.

Os componentes têm o seu estilo definido. O botão, por exemplo, tem sua cor de fundo, cor do texto e tamanho de fonte, mas eventualmente seria necessário alterar estas definições e usar este componente com algumas personalizações. Além disso, tem que ter o dark mode. Né? E como poderíamos fazer isso? Mais uma coisa a definir.

Para alterar o estilo que já vem no componente poderíamos receber parâmetros no construtor do widget e assim determinar as alterações. Pensando no botão, poderíamos ter uma propriedade chamada backgroundColor e fontColor. Quando valores são passados para estas propriedades alteraríamos a cor de fundo do botão e a cor da fonte do botão respectivamente e se estes parâmetros forem ignorados utilizaria o estilo padrão do componente. Isso funciona, mas traria grandes desvantagens:

  • Aumenta a complexidade dos fontes dos produtos, visto que muitas propriedades relacionadas ao tema dos componentes teriam que ser passados na construção das views.
  • Obrigaria que as propriedades de estilo tivessem que ser repetidas em todos as instâncias dos componentes e frequentemente as alterações seriam as mesmas para todas as instâncias de um mesmo widget.
  • Não resolve o gerenciamento de temas, que teria que ser controlada pelos próprios produtos. Disponibilizar um tema claro e escuro seria total responsabilidade dos aplicativos e a nossa biblioteca não teria controle sobre os temas.

Diante das desvantagens eu não poderia seguir com esta abordagem. A biblioteca precisava ter controle do gerenciamento de temas, definir o estilo de um tema claro e também de um tema escuro. Os aplicativos iriam dizer qual tema iria usar e pronto, os componentes iriam alterar para se adaptar a este tema. Parece perfeito, mas tem mais um porém. A biblioteca precisa disponibilizar o tema claro e também o tema escuro, mas também temas personalizados que seriam criados pelos aplicativos, afinal de contas este era um requisito inicial.

Para isso me inspirei no que o Flutter faz. No MaterialApp podemos passar uma instância de ThemeData para theme e darkTheme e assim o tema informado vai funcionar para todos o aplicativo. Eu precisava fazer o mesmo para os meus componentes. Não daria para usar o mesmo ThemeData porque meus componentes são específicos e com suas próprias características. Aí criei um ThemeData próprio que aqui chamarei de MyThemeData. Todos os componentes passariam a ter uma classe de tema e o MyThemeData agruparia todos os temas e também o ThemeData nativo, ou seja, dentro do MyThemeData teria um MyButtonThemeData, MyCardThemeData, etc. Dentro do MyButtonThemeData teria as definições de cor de fundo, cor da fonte.

Agora os aplicativos poderiam criar os temas a partir da MyThemeData, mas como passar estes temas para os meus componentes. Para isso criei um widget que deveria ser usado no topo da árvore de widgets e que apenas gerenciaria este tema. Vou chamá-lo aqui de MyDesignSystem. Este widget recebe um parâmetro theme. Para gerenciar este tema usei o Provider mesmo.

void main() {
runApp(
MyDesignSystem(
theme: MY_LIGHT_THEME,
child: MyApp(),
),
);
}

Tudo estava funcionando agora. Podia criar os temas e passar os temas para o MyDesignSystem e quando alterar o tema no app os componentes podiam consultar o tema ativo no provider e alterar automaticamente seus estilos de acordo com este tema. Mas ainda faltava uma coisa. Se eu defino no tema que o meu botão vai ter uma cor diferente do padrão, por exemplo vermelho, todos os botões ficariam vermelhos quando o tema estivesse ativo. Mas e se eu quisesse que apenas um botão no meu aplicativo ficasse azul? Aí poderíamos usar os parâmetros backgroundColor diretamente no widget. Certo? Não seria muito legal porque a lógica ficaria repetida e quando tivesse que criar um widget novo precisaria criar as mesmas propriedades no tema dele e também no construtor. Para resolver eu adicionei um nova camada entre o tema e o widget, o style. Assim um MyButton teria uma classe MyButtonStyle e uma classe MyButtonTheme. A classe MyButtonTheme teria um atributo MyButtonStyle, assim como o MyButtonWidget e no no MyButtonStyle adiociono as propriedades de estilo, backgroundColor e fontColor.

class MyButtonStyle {
const MyButtonStyle({
this.backgroundColor,
this.fontColor,
});

final Color? backgroundColor;
final Color? fontColor;
}

class MyButtonTheme {
const MyButtonTheme({
this.style,
});

final MyButtonStyle? style;
}

class MyButtonWidget {
const MyButton({
this.style,
});

final MyButtonStyle? style;
}

Na hora de construir o widget eu apenas coloco uma ordem de prioridade, Usando primeiramente o style parâmetro do widget, depois o style do tema atual e por último um estilo padrão de fallback.

widget style > theme style > fallback style

return Container({ 
color: style.backgroundColor
?? theme.myButtonTheme.style.backgroundColor
?? MyColors.primaryColor500,
child: Text(
label,
style: TextStyle(
color: style.fontColor
?? theme.myButtonTheme.style.fontColor
?? MyColors.grayscale700,
),
),
});

Agora está tudo finalizado e funcionando. Temos componentes reaproveitáveis, uma estrutura para criar temas personalizados para estes componentes e também um gerenciamento próprio da biblioteca.

Esta foi a minha experiência criando uma biblioteca de componentes seguindo um design system. Claro que aqui está tudo muito resumido, mas a ideia era apresentar como foi a minha abordagem e os passos que realizei na construção. Até mais ;)

--

--