Construyendo sistemas de diseño en Flutter

- Andrés Cruz

Construyendo sistemas de diseño en Flutter

A diferencia de lo que su nombre puede implicar, las reglas de los sistemas de diseño no se aplican solo a los diseñadores. El valor real de un sistema de diseño bien construido e implementado es imponer estas reglas a los diseñadores y desarrolladores por igual, creando una consistencia que permita a ambas disciplinas enfocarse en desafíos de mayor nivel e ignorar el tedio de las piezas compositivas que forman una aplicación cohesiva.

Es probable que las empresas exitosas en diseño tengan un sistema de diseño. Esto les permite centrarse en la identidad y la funcionalidad de sus aplicaciones sin tener que pensar en piezas individuales como botones, pancartas, texto, etc.

Como diseñador, no quiero comenzar a diseñar una página y tener que pensar qué es un botón y crear especificaciones exhaustivas para el consumo de los desarrolladores. Como desarrollador, no quiero recibir un diseño y ver un botón que tengo que construir desde cero por décima vez. Un buen sistema de diseño y su contraparte implementada (en nuestro caso, una biblioteca de Flutter) resuelven estos problemas.

Nombrar es importante

Como desarrolladores, sabemos que la semántica es increíblemente importante para la adopción y la incorporación. Pero en el caso de los sistemas de diseño, esto se extiende a nuestros amigables diseñadores de barrio. Estamos creando un idioma compartido y, por lo tanto, deberíamos implementar nuestros widgets usando ese idioma.

Esfuércese por usar los mismos nombres para sus widgets que los diseñadores cuando describen los componentes. Esto asegura que no haya errores de comunicación cuando se trabaja de un lado a otro entre el diseño y la implementación. Aplique esta metodología también a la configuración de sus widgets. Si un botón tiene un ícono "principal" y "posterior" en lugar de "izquierda" y "derecha", use la misma terminología en los campos de su clase de widget. En el caso de los botones de MyDesign, podemos ver que reflejan botones rellenos (elevados) y delineados con Material, pero se denominan principal y secundario. Nuestra implementación debe respetar ese esquema de nombres.

Las implementaciones de sistemas de diseño a menudo viven independientemente de las aplicaciones que admiten. Este es un desacoplamiento beneficioso para que la lógica comercial no se filtre en la implementación del diseño, y su biblioteca se puede reutilizar como una dependencia en múltiples aplicaciones de consumo. Descubrí que debido a esto, un prefijo para widgets proporcionado por el sistema de diseño también es beneficioso. MyButton en lugar de Button ayuda a distinguir qué widgets en una aplicación son locales y cuáles no, además de evitar conflictos de nombres con los muchos widgets que proporcionan Flutter y otras dependencias de aplicaciones.

Definir tokens de diseño de forma independiente

Los tokens de diseño son los "primitivos" de un sistema de diseño: valores constantes reutilizados en todos los componentes y estándares definidos. Elementos como colores, espacios, estilos de texto e íconos a menudo se incluyen en los sistemas de diseño como "tokens" y son buenos candidatos para definirlos independientemente de los widgets que los usan. El diseño de materiales ya hace esto con las clases Colores e Iconos, así como con el tema de texto del objeto Tema predeterminado que contiene tratamientos de texto predefinidos como estilos de cuerpo y título.

class MyColors {
  MyColors._();
  
  static const Color dark = Color(0xff222222);
  static const Color white = Color(0xffffffff);
  static const Color blue = Color(0xff0000ff);
  static const Color red = Color(0xffff0000);
  static const Color green = Color(0xff00ff00);
  static const Color lightBlue = Color(0xffaaaaff);
}

Usar composición

Flutter se basa en una componibilidad agresiva, y su biblioteca de sistemas de diseño también debería serlo. Utilice el extenso catálogo de widgets de Material Design y Cupertino de Flutter y diseñelos para que coincidan con las especificaciones de su sistema. Esto abstrae el estilo de todos los lugares en los que se utilizarán los widgets. Evite repetir el código en los widgets creando widgets más pequeños que pueda componer en otros. Con los botones de MyDesign, compondremos los botones de Material como base. En sistemas más complejos, puede tener sus propios componentes básicos, que se pueden componer en muchos otros.

Menos es más

Intenta que tus widgets sean lo más simples posible. Suscríbase al principio del mínimo conocimiento: un widget solo debe consumir exactamente las entradas que necesita para mostrarse y funcionar correctamente. Esto también significa que su widget no se puede usar de formas inesperadas.

Si está utilizando un widget Material y lo diseña para su diseño, es probable que no necesite todas las configuraciones que permite el widget. Los widgets materiales están destinados a ser altamente configurables, pero es posible que sus widgets no lo sean. ¡Reduce esos parámetros!

Algunos widgets como ElevatedButton son extremadamente flexibles en lo que se puede pasar como elementos secundarios (se necesita cualquier Widget). En MyDesign, los botones solo permiten texto e íconos como elementos secundarios, por lo que volveremos a escribir los campos de nuestro widget para asegurarnos de que solo se acepten valores válidos y permitir que la función de compilación abstraiga las complejidades de crear las partes internas del botón.

class MyButton extends StatelessWidget {
  final String? label;
  final IconData? icon;
  final VoidCallback? onPressed;
  const MyButton({
    super.key,
    this.label,
    this.icon,
    this.onPressed,
  }) : assert(label != null || icon != null, 'Label or icon must be provided.');
  // Use asserts to enforce rules which cannot be done at compile time
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      // Pass through parameters which are still necessary
      onPressed: onPressed,
      child: Row(
        mainAxisSize: MainAxisSize.min,
        mainAxisAlignment: MainAxisAlignment.center,
        // Icon, icon + text, and text-only possibilities are abstracted in MyButton
        children: [
          if (icon != null) Icon(icon, size: 18.0),
          if (icon != null && label != null)
            const SizedBox(width: MySpacing.x0L),
          if (label != null) Text(label!, textAlign: TextAlign.center),
        ],
      ),
    );
  }
}

Usar enums para hacer cumplir entradas válidas

Este consejo sigue como una extensión de la recomendación anterior. En muchos casos, los tipos de campo de un widget pueden permitir valores que no se ajustan a las reglas de su sistema de diseño. Por ejemplo, los botones de MyDesign solo permiten un subconjunto de colores de la paleta de la marca. En lugar de que nuestro widget tome un parámetro de tipo Color (y, por lo tanto, permita botones con colores incorrectos), crearemos una enumeración que represente y restrinja la configuración a aquellos que están permitidos.

Antes de las enumeraciones mejoradas de Dart 2.17, esto causaba la pequeña molestia de tener que mapear estos valores de enumeración de nuevo a un tipo válido en nuestro constructor o función de compilación usando un caso Map o switch. Sin embargo, con enumeraciones mejoradas, podemos definir enumeraciones con campos finales que conectan cada valor de enumeración con su valor representado.

// Restrict color inputs to MyButton using an enum.
enum MyButtonColor {
  red(MyColors.red),
  blue(MyColors.blue),
  green(MyColors.green);
  final Color color;
  const MyButtonColor(this.color);
}
class MyButton extends StatelessWidget {
  final String? label;
  final IconData? icon;
  final MyButtonColor color;
  final VoidCallback? onPressed;
  const MyButton({
    super.key,
    this.label,
    this.icon,
    this.color = MyButtonColor.blue,
    this.onPressed,
  }) : assert(label != null || icon != null, 'Label or icon must be provided.');
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      // A contrived example using styleFrom to build a ButtonStyle from the background color
      style: ElevatedButton.styleFrom(backgroundColor: color.color),
      onPressed: onPressed,
      child: Row(
        mainAxisSize: MainAxisSize.min,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          if (icon != null) Icon(icon, size: 18.0),
          if (icon != null && label != null)
            const SizedBox(width: MySpacing.x0L),
          if (label != null) Text(label!, textAlign: TextAlign.center),
        ],
      ),
    );
  }
}

Heredar estilos

Siempre es una buena idea no codificar valores. Principalmente nos hemos ocupado de eso separando los tokens de diseño en constantes. Sin embargo, el uso de MyColors.blue en lugar de Color(0xff0000ff) en todo nuestro código aún puede dejar nuestro sistema de diseño innecesariamente restringido. Si bien nuestros tokens permiten un lugar único para editar valores si el sistema de diseño necesita cambios, ¿qué pasa si necesitamos un tema completamente nuevo? Esta es una práctica común con temas claros y oscuros en las aplicaciones.

Las variables constantes estáticas no pueden resolver la preferencia del usuario en el tema. Entonces, al igual que el diseño de materiales, use la herencia de estilos del tema global modificando las propiedades, viene con o creando extensiones de tema.

Usar constructores con nombre

Esto puede deberse a una preferencia personal, pero los constructores con nombre de dart pueden ayudar a reducir el modelo y mejorar la semántica de sus widgets.

Mirando nuestros botones primario y secundario de MyDesign, sabemos que hay código reutilizado en la construcción de las partes internas, pero aún necesitaremos un Material ElevatedButton y un OutlineButton respectivamente. Podríamos crear dos widgets separados MyPrimaryButton y MySecondaryButton y extraer un widget para construir los elementos secundarios. Alternativamente, podríamos tener un solo widget con constructores con nombre, MyButton.primary y MyButton.secondary.

Estos constructores pueden establecer un campo privado que nos dice qué hacer en nuestro método de construcción. Este enfoque se vuelve más valioso con múltiples widgets a medida que la lógica y el método de construcción aumentan la complejidad (compartir código es más fácil que coordinar la comunicación de múltiples widgets). También puede ser valioso "agrupar" variantes de widgets de esta manera con fines semánticos. Con finalización de código IDE, escribiendo MyButton. brinda un "catálogo" de las variantes disponibles, un beneficio que no se recibe a través de un enfoque de múltiples widgets.

class MyButton extends StatelessWidget {
  final String? label;
  final IconData? icon;
  final MyButtonColor color;
  final VoidCallback? onPressed;
  final bool _primary;
  const MyButton.primary({
    super.key,
    this.label,
    this.icon,
    this.color = MyButtonColor.blue,
    this.onPressed,
  })  : _primary = true,
        assert(
          label != null || icon != null,
          'Label or icon must be provided.',
        );
  const MyButton.secondary({
    super.key,
    this.label,
    this.icon,
    this.color = MyButtonColor.blue,
    this.onPressed,
  })  : _primary = false,
        assert(
          label != null || icon != null,
          'Label or icon must be provided.',
        );
  @override
  Widget build(BuildContext context) {
    final child = Row(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        if (icon != null) Icon(icon, size: 18.0),
        if (icon != null && label != null) const SizedBox(width: MySpacing.x0L),
        if (label != null) Text(label!, textAlign: TextAlign.center),
      ],
    );
    if (_primary) {
      return ElevatedButton(
        style: ElevatedButton.styleFrom(backgroundColor: color.color),
        onPressed: onPressed,
        child: child,
      );
    } else {
      return OutlinedButton(
        style: OutlinedButton.styleFrom(backgroundColor: color.color),
        onPressed: onPressed,
        child: child,
      );
    }
  }
}

Conclusión

Puedes ser tan estricto con tu sistema de diseño como quieras. Confiar en que los desarrolladores se adhieran a las reglas escritas o habladas puede ser suficiente en muchas circunstancias. Pero crear widgets que aprovechen la terminología compartida con los diseñadores y apliquen estrictamente las reglas del sistema puede conducir a una transferencia más rápida del diseño al desarrollador y una incorporación más fácil para los futuros desarrolladores.

 

https://betterprogramming.pub/building-design-systems-in-flutter-d52d66004070

Andrés Cruz

Desarrollo con Laravel, Django, Flask, CodeIgniter, HTML5, CSS3, MySQL, JavaScript, Vue, Android, iOS, Flutter

Andrés Cruz en Udemy

Acepto recibir anuncios de interes sobre este Blog.