Como melhoramos uma API interna usando Flutter: processo e aprendizado na visão de um dev junior

Como melhoramos uma API interna usando Flutter: processo e aprendizado na visão de um dev junior

A intenção deste post é dar uma amostra para pessoas que, como eu, estão em uma fase de transição de carreira sobre como é pensar em melhorias de código e como funciona a discussão durante esse processo.

O problema

Na Revelo, como parte do nosso design system, utilizamos Badges (crachás, insígnias ou distintivos) para ressaltar informações relevantes no nosso app. Portanto temos, como vocês já devem imaginar, vários estilos diferentes de Badges em várias partes do app.

Focamos sempre em entregas de altíssima qualidade e queremos evitar erros de design que possam ser inseridos acidentalmente por nós mesmos no momento do desenvolvimento de novas funcionalidades ou até de refatoração de funcionalidades mais antigas.

Por conta disso, ter uma API interna que nos ajude a evitar tais erros, padronizar o código e facilitar a inserção de novos modelos de design é super importante, certo? Certo! Olhem como estava a nossa Badge antes:

class ReveloBadge extends StatelessWidget {
  final String text;
  final Color backgroundColor;
  final Color textColor;

  const ReveloBadge({
    Key key,
    @required this.text,
    @required this.backgroundColor,
    this.textColor,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
      decoration: BoxDecoration(
        color: backgroundColor,
        borderRadius: BorderRadius.circular(4),
      ),
      child: Text(
        text.toUpperCase(),
        style: ReveloTheme.of(context).overline.copyWith(
          color: textColor ?? ReveloColors.white,
        ),
      ),
    );
  }
}
Github: https://gist.github.com/gabrielaraujoz/520cee151509c6c69f1ff32205cbb77f#file-oldrevelobadge-dart

Na nossa visão, esse modelo de Badge deixaria qualquer desenvolvedor com uma escolha muito aberta em relação a parâmetros importantes como a backgroundColor (cor de fundo da Badge) e a textColor (cor do texto). Além disso, não conseguíamos configurar as bordas caso fosse necessário.

O mais legal de tudo? Pude pegar esse desafio para mim!

Solução 1

Meu ponto de partida foi ir mais a fundo no código do Flutter, entender mais sobre como são feitos os Widgets do próprio framework. Minha ideia era que eu já tinha visto algo semelhante no código, então precisaria entender melhor como as coisas são feitas “por trás dos panos” para poder propor uma solução.

Decidi investigar os botões e acabei chegando no FlatButton, que estende do MaterialButton (isso já me deu uma pequena luz). Acontece que o FlatButton tem exatamente a primeira solução que pensei: um construtor FlatButton.icon que faz o widget principal aceitar um ícone dentro dele, mesmo que o FlatButton não tenha esse paramêtro no seu construtor.

Long story short: O FlatButton.icon é uma classe com construtor própio, que herda do FlatButton e utiliza um Mixin do MaterialButton para poder utilizar sua própria instância do ButtonTheme.

Essa parte ficou bem complicada para mim e acabei não entendendo muito bem o uso do Mixin antes de utilizá-lo, mas mesmo assim utilizei e criei classes novas para a nossa Badge como ReveloBagde.transparent, ReveloBadge.red, etc., utilizando essa estratégia.

Funcionou!

Mas o nosso código ficou gigante e complexo, por isso fica aqui o primeiro aprendizado: é muito importante entender o que o código faz exatamente antes de adaptá-lo para uma solução própria.
Segundo aprendizado: git commit é muito bom, use sempre! Queria trazer o código dessa versão aqui para vocês mas não commitei e perdi :(

Para pelo menos ilustrar um pouco, deixo o link do Github do Flutter com o código fonte do FlatButton: Código fonte FlatButton Widget.

Solução 2

Trouxe o resultado do meu estudo e implementação para o Cesar Castro e o Douglas Iacovelli, meus mentores e companheiros de squad na Revelo, para saber o que achavam da minha solução.

Ela funcionava, de fato, mas não resolvia um dos nossos principais focos: nos ajudava em relação a evitar erros na inserção dos parâmetros pois as classes estavam “travadas” mas era de difícil manutenção/atualização. Em cada mudança de design, precisaríamos alterar uma classe inteira para poder adaptar a Badge. E se precisássemos de uma Badge com borda E fundo colorido? E se a Badge transparente se tornasse uma com cor de fundo diferente?

Com base nessa solução complexa, chegamos a uma que se tornou muito mais simples e, na minha humilde opinião, bem bonita!

Aqui fica o terceiro aprendizado: esteja sempre aberto a discutir o seu código, aceitar propostas de melhoria e abraçar todo o aprendizado que vem com isso.

Decidimos então criar uma classe de estilos com construtor privado (não pode ser declarada com construtor fora dela mesma) e utilizá-la com factories para termos todas as opções de estilo das Badges travadas em um só lugar. Isso ajuda muito na manutenção do código, sério.

Para ilustrar, veja a classe de estilos que criamos:

class ReveloBadgeStyle extends Equatable {
  final Color backgroundColor;
  final Color textColor;
  final Border border;

  const ReveloBadgeStyle._({
    @required this.backgroundColor,
    @required this.textColor,
    this.border,
  });

  @override
  List<Object> get props => [backgroundColor, textColor, border];
  factory ReveloBadgeStyle.red() => ReveloBadgeStyle._(
        backgroundColor: ReveloColors.red,
        textColor: ReveloColors.white,
      );
  factory ReveloBadgeStyle.blue() => ReveloBadgeStyle._(
        backgroundColor: ReveloColors.blueLight,
        textColor: ReveloColors.white,
      );
  factory ReveloBadgeStyle.blueLight() => ReveloBadgeStyle._(
        backgroundColor: ReveloColors.blueLight,
        textColor: ReveloColors.white,
      );

  factory ReveloBadgeStyle.green() => ReveloBadgeStyle._(
        backgroundColor: ReveloColors.green,
        textColor: ReveloColors.white,
      );
  factory ReveloBadgeStyle.gray() => ReveloBadgeStyle._(
        backgroundColor: ReveloColors.gray,
        textColor: ReveloColors.white,
      );
  factory ReveloBadgeStyle.outlined(BuildContext context) => ReveloBadgeStyle._(
        backgroundColor: ReveloColors.whiteTransparent,
        textColor: ReveloTheme.of(context).highEmphasisColor,
        border: Border.all(color: ReveloTheme.of(context).chipOutlineColor),
      );
}
Github: https://gist.github.com/gabrielaraujoz/ec50c7f7dee9d71c29e52f1c31e1cde5#file-newrevelobadge-dart

Como vocês podem ver, cada estilo tem as suas cores de fundo e texto já definidas e quando necessário conseguimos definir uma borda também, vide o estilo .outlined.

Mas só isso também não resolveria o nosso problema, pois ainda teríamos a inserção de um estilo na declaração da Badge, então fomos mais a fundo e utilizamos a mesma estratégia de Factories para termos construtores específicos para cada um deles, como ReveloBadge.blue e ReveloBadge.outlined.

Agora sim, temos um widget que se tornou uma API interna, pode ser utilizado em todo o código com menos risco de fugir do Design System e e maior velocidade de desenvolvimento.

Para finalizarmos, aqui está o código da nossa classe ReveloBadge refatorada e pronta para uso:

class ReveloBadge extends StatelessWidget {
  final String text;
  final String iconName;
  final ReveloBadgeStyle badgeStyle;
  
  const ReveloBadge({
    Key key,
    @required this.text,
    @required this.badgeStyle,
    this.iconName,
  }) : super(key: key);

  factory ReveloBadge.outlined({
    Key key,
    @required String text,
    @required BuildContext context,
    String iconName,
  }) =>
      ReveloBadge(
        key: key,
        text: text,
        badgeStyle: ReveloBadgeStyle.outlined(context),
        iconName: iconName,
      );

  factory ReveloBadge.red({
    Key key,
    @required String text,
    String iconName,
  }) =>
      ReveloBadge(
        key: key,
        text: text,
        iconName: iconName,
        badgeStyle: ReveloBadgeStyle.red(),
      );

  factory ReveloBadge.blue({
    Key key,
    @required String text,
    String iconName,
  }) =>
      ReveloBadge(
        key: key,
        text: text,
        iconName: iconName,
        badgeStyle: ReveloBadgeStyle.blueLight(),
      );

  factory ReveloBadge.green({
    Key key,
    @required String text,
    String iconName,
  }) =>
      ReveloBadge(
        key: key,
        text: text,
        iconName: iconName,
        badgeStyle: ReveloBadgeStyle.green(),
      );

  factory ReveloBadge.gray({
    Key key,
    @required String text,
    String iconName,
  }) =>
      ReveloBadge(
        key: key,
        text: text,
        iconName: iconName,
        badgeStyle: ReveloBadgeStyle.gray(),
      );

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: Dimens.sm, vertical: 2),
      decoration: BoxDecoration(
        color: badgeStyle.backgroundColor,
        borderRadius: BorderRadius.circular(Dimens.borderRadiusSmaller),
        border: badgeStyle.border,
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          if (iconName != null)
            Padding(
              padding: const EdgeInsets.only(right: Dimens.md),
              child: SafeIcon( // Classe própria que exibe um ícone a partir de um nome
                assetName: iconName,
                color: badgeStyle.textColor,
                size: Size.square(12),
              ),
            ),
          Flexible(
            child: Text(
              text.toUpperCase(),
              style: ReveloTheme.of(context).overline.copyWith(
                    color: badgeStyle.textColor,
                  ),
              maxLines: 2,
              overflow: TextOverflow.ellipsis,
              softWrap: true,
            ),
          ),
        ],
      ),
    );
  }
}
Github: https://gist.github.com/gabrielaraujoz/ec50c7f7dee9d71c29e52f1c31e1cde5#file-newrevelobadge-dart

TL;DR

Novos devs: não tenham medo de ir fundo no código do framework que estão aprendendo e entender o que el está fazendo de verdade. Ouçam seus companheiros de equipe que têm mais experiência e não tenham muito apego em relação ao código que fizerem. Valorizem cada oportunidade dessas como um intenso aprendizado e internalizem o modo de pensar deles. Isso é super importante na minha sincera opinião e acredito que vá ajudá-los a se desenvolverem muito mais rapidamente. E usem git commit sem dó.

Devs experientes: dêem desafios e oportunidades como essa para os juniores dos seus times, estejam abertos a ouvir e discutir as soluções propostas e os estimulem a procurar sempre novas e melhores soluções sozinhos, mas sempre os apoiem quando necessário — isso vai ajudá-los a crescer muito mais rápido!