Poniendo los métodos build a dieta: trucos y consejos para un código de UI mas limpio en Flutter
Primero que nada quiero aclarar que este articulo fue escrito por Liro Krankka y yo solo estoy haciendo una traducción autorizada, al final del articulo estará el links al artículo original utilizado.
Flutter es genial - Rockea.
Tenemos modernas y frescas APIs para construir complejas Interfaces de Usuario (User interface, UI) en una pequeña cantidad de código. El hot reload es genial - podemos estar a 5 pantallas de profundidad en nuestra jerarquía de navegación, hacer unos cambios en la UI, presionar crtl + S y la UI cambiara en menos de un segundo sin perder el estado. Pero cuando se trata de crear apps complejas, terminamos teniendo un montón de código de UI.
Con mas código, viene mas responsabilidad de mantenerlo legible. Mantener el código legible lo hace mas fácil de mantener a la larga. Vamos a ver un par de tips rápidos sobre como mantener nuestro código de UI mas legible.
Problema #1 - “Padding, Padding, Padding”
La mayoría de los diseños en nuestras aplicaciones están basados en contenido puesto horizontal o verticalmente. Esto significa que varias veces usaremos los widgets Column o Row.
Ya que poner widgets justo debajo o a lado del otro no siempre se ve bien, queremos tener márgenes entre ellos. Una de las formas mas obvias de poner margen entre dos widgets es envolver a uno de ellos dentro de un widget Padding.
Considera el siguiente ejemplo:
Column(
children: [
Text('Primera linea de texto.'),
const Padding( // Padding agregado con margen superior de 8.0
padding: EdgeInsets.only(top: 8.0),
child: Text('Segunda linea de texto.'),
),
const Padding( // Padding agregado con margen superior de 8.0
padding: EdgeInsets.only(top: 8.0),
child: Text('Tercera linea de texto.'),
),
],
),
Tenemos tres Text widgets dentro de un Column, estos tienen 8.0 de margen entre ellos.
El Problema: “Widgets Ocultos”
El problema de usar widgets Padding en todos lados es que empiezan a obscurecer la “Lógica de Negocio” de nuestro código de UI. Aumentan el ruido visual agregando niveles de indentación y numero de lineas.
Lo que queremos hacer es que los widgets principales resalten lo mas posible. Cada nivel de indentación adicional cuenta. Si podemos reducir el conteo de lineas al mismo tiempo, eso seria genial también.
La solución: Usar SizedBoxes
Para combatir el problema de los widgets ocultos, podemos reemplazar todos los Paddings con SizedBoxes widgets, Usar SizedBoxes en lugar de Paddings nos permite reducir el nivel de indentación y el conteo de lineas:
Column(
children: [
Text('Primera linea de texto.'),
const SizedBox(height: 8.0),
Text('Segunda linea de texto.'),
const SizedBox(height: 8.0),
Text('Tercera Linea de texto.'),
],
),
Aquí los widgets SizedBox cumplen la misma función de separar los Text con un margen de 8.0.
El mismo enfoque se puede usar con los widgets Row, ya que los Row acomodan sus widgets children horizontalmente, podemos usar la propiedad width de los SizedBox para los márgenes horizontales en lugar de height.
Problema #2 - Llamadas de regreso demasiado juntas
Taps o toques regularmente son la manera mas común del usuario de interactuar con nuestras aplicaciones.
Para permitir al usuario “tapear” algún lugar en nuestra aplicación, podemos usar un widget GestureDetector. para usarlo hay que envolver el widget original y especificar la llamada de regreso en la propiedad onTap en el constructor del GestureDetector.
Considera el siguiente ejemplo tomado de la aplicación (Creada por el escritor original) inKino app:
...
final List<Event> events;
@override
Widget build(BuildContext context) {
return GridView.builder(
...
itemBuilder: (_, int index) {
final event = events[index];
return GestureDetector(
onTap: () {
// :-(
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => EventDetailsPage(event),
),
);
},
child: EventGridItem(event: event),
);
},
);
}
La aplicación inKino tiene un Grid de posters de películas. cuando el usuario toca una de ellas, debería ser llevado a la pagina de detalles de la película.
El problema: Mezclar código de UI con Lógica
Nuestro método build solo debería contener lo mínimo relacionado con construir la UI de nuestra aplicación. La lógica contenida en el onTap no esta relacionada con con construir la UI, esto agrega ruido innecesario al método build.
En este caso, podemos determinar rápidamente que Navigator.push mete una nueva ruta y esta es EventDetailsPage así que tocando un elemento del Grid abre su pagina de detalles. Como sea, si la llamada del onTap está involucrada, esto podría requerir un poco mas de lectura para entender el método build.
La solución: Extraer la lógica a un método privado
Este problema puede ser solucionado extrayendo la llamada del onTap en un bien nombrado método privado. En este caso creamos un método llamado _openEventDetails:
...
final List<Event> events;
void _openEventDetails(Event event) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => EventDetailsPage(event),
),
);
}
@override
Widget build(BuildContext context) {
return GridView.builder(
...
itemBuilder: (_, int index) {
final event = events[index];
return GestureDetector(
// :-)
onTap: () => _openEventDetails(event),
child: EventGridItem(event: event),
);
},
);
}
Esto está mejor.
Ya que la llamada del onTap se extrajo a un método bien nombrado, no tenemos que leer todo el código completo. ahora es fácil entender que pasa cuando la llamada del onTap es invocada, solo con leer el nombre del método.
Cambien reducimos la cantidad de lineas en nuestro preciado método build y nos concentramos en solo leer el código de UI.
Problema #3: ifs en todos lados
Algunas veces, los widgets children de nuestras Column o Row no deberían ser visibles. Por ejemplo, siguiendo usando como ejemplo inKino, si una película no tiene detalles del argumento por alguna razón, no tiene sentido mostrar un Text vació en nuestra UI.
Una forma común de acondicionar los children de Column o Row se ve así:
class EventDetailsPage extends StatelessWidget {
EventDetailsPage(this.event);
final Event event;
@override
Widget build(BuildContext context) {
//Lista de widgets que serán los children del Column
final children = <Widget>[
_HeaderWidget(),
]; //ifs que agregan o no un widget a la lista de children
// :-(
if (event.storyline != null) {
children.add(StorylineWidget(...));
}
// :-(
if (event.actors.isNotEmpty) {
children.add(ActorList(...));
}
return Scaffold(
...
//Columna que usará los widgets de la lista como children
body: Column(children: children),
);
}
}
La esencia para agregar elementos de manera condicional a una Column es bastante simple: inicializamos una lista local de widgets, y si alguno cumple con sus condiciones lo agregamos a la lista. Finalmente le pasamos esa lista al parámetro children del Column.
En este caso, la API de Finnkino (API usada en la app inKino) no siempre regresa los detalles del argumento o los actores de la película.
El problema: ifs en todos lados
Aunque esto funciona, esos ifs se harán viejos muy rápidamente.
A pesar de que son fáciles de entender, toman espacio innecesario en nuestro método build. Especialmente si tenemos tres o mas.
La solución: un método global dentro de utils.
Para combatir el problema, podemos crear un método útil global que condiciona los widgets a agregar. Lo siguiente es un patrón usado en el código de Framework principal de Flutter.
En el folder lib/utils/methods_utils.dartvoid addIfNonNull(Widget child, List children) {
if (child != null) {
children.add(child);
}
}
En lugar de repetir la lógica condicional de agregar widgets a la lista children, creamos un método útil global que la contenga.
Una vez que definimos el método solo basta con importar el archivo y usar el método global.
import 'methods_utils.dart';
class EventDetailsPage extends StatelessWidget {
EventDetailsPage(this.event);
final Event event;
Widget _buildStoryline() =>
event.storyline != null ? StorylineWidget(...) : null;
Widget _buildActorList() =>
event.actors.isNotEmpty ? ActorList(...) : null;
@override
Widget build(BuildContext context) {
final children = <Widget>[
_HeaderWidget(),
];
// :-)
addIfNonNull(_buildStoryline(), children);
addIfNonNull(_buildActorList(), children);
return Scaffold(
...
body: Column(children: children),
);
}
}
Lo que hicimos aquí es que ahora nuestro método _buildMywidget() regresa un widget o null, dependiendo de si la condición es verdadera o no. Esto nos permite ahorrar espacio en nuestro método build, especialmente si tenemos muchos widgets condicionados.
Problema #4 - Infierno de paréntesis
Hay que guardar el mejor para el final.
Este quizá sea uno de los problemas mas prevalecientes en nuestro código de diseño. Una queja común en el código de UI con Flutter es que los niveles de indentación aumentan como locos, lo que produce muchos paréntesis.
Considera el siguiente ejemplo:
...
@override
Widget build(BuildContext context) {
final backgroundColor =
useAlternateBackground ? const Color(0xFFF5F5F5) : Colors.white;
return Material(
color: backgroundColor,
child: InkWell(
onTap: () => _navigateToEventDetails(context),
child: Padding(
padding: const EdgeInsets.symmetric(...),
child: Row(
children: [
Column(
children: [
Text(
hoursAndMins.format(show.start),
style: const TextStyle(...),
),
Text(
hoursAndMins.format(show.end),
style: const TextStyle(...),
),
],
),
const SizedBox(width: 20.0),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
show.title,
style: const TextStyle(...),
),
const SizedBox(height: 4.0),
Text(show.theaterAndAuditorium),
const SizedBox(height: 8.0),
Container(
// Presentation method chip.
// Styling redacted for brevity ...
child: Text(show.presentationMethod),
),
],
),
),
],
),
),
),
);
}
El ejemplo anterior es de la aplicación inKino, y contiene el código para construir una lista de películas con sus horarios. Lo hice feo apropósito (el escritor original). Bastante ha sido reducido créeme cuando digo que el ejemplo completo hubiera sido algo grande.
En esencia, este es el código para mostrar estos chicos malos:
Si estas leyendo esto en un dispositivo móvil, lo siento. El código superior no es lindo de ver ni siquiera en pantallas grandes. ¿Por Qué? estoy bastante seguro que la mayoría de ustedes ya sabe.
El problema: ¿Algunas vez usaste Lisp?
Este viejo lenguaje de programación, llamado Lisp, tiene una sintaxis que hace uso de muchos paréntesis. He visto interfaces Flutter ser comparadas con Lisp bastantes veces, y para ser honesto, puedo ver la similitud.
Es sorprendente como nadie ha hecho esto antes, así que aquí vamos.
“Como rescatar a la princesa con Flutter”
A pesar de que el código superior funciona, es bastante feo de ver. Los niveles de indentación llegan bastante lejos, hay mucho desorden vertical, paréntesis, y es difícil seguir todo lo que esta pasando y donde esta pasando.
Solo mira los paréntesis del final:
),
),
),
],
),
),
],
),
),
),
);
}
Por un anidado tan profundo, incluso con un buen IDE, es difícil agregar nuevos elementos a nuestro diseño. Sin mencionar, leer el código de UI.
Solución: refactorizar distintas partes de la UI en widgets separados
Nota del traductor: La version original del articulo mencionaba el uso de métodos en lugar de clases, pero ya que el usuario Wm Leler mencionó, en los comentarios del articulo original, que usar métodos es un anti patrón en el desempeño de las aplicaciones Flutter él autor original actualizo su articulo y creo otro dedicado a este tema (estoy trabajando en la traducción). Lo siguiente es la traducción del articulo ya actualizado.
Hay dos partes distintas en nuestra lista: la izquierda y la derecha.
La parte izquierda contiene la información sobre l ahora de inicio y de termino de las películas. El lado derecho tiene información como el titulo y si es en formato 2D o 3D. Para hacer el código mas legible, vamos a empezar por separar eso en dos widgets llamados _LeftPart y _RightPart.
Ya que el widget que muestra el tipo de presentación, dentro del lado derecho, va a introducir mucho desorden vertical y anidado profundo lo separaremos en otro widget llamado _PresentationMethod. Nota del autor original: no separes tu método build en diferentes métodos eso es un anti patrón de desempeño y merece su propio articulo.
...
@override
Widget build(BuildContext context) {
final backgroundColor =
useAlternateBackground ? const Color(0xFFF5F5F5) : Colors.white;
return Material(
color: backgroundColor,
child: InkWell(
onTap: () => _navigateToEventDetails(context),
child: Padding(
padding: const EdgeInsets.symmetric(...),
child: Row(
children: [
_LeftPart(),
const SizedBox(width: 20.0),
_RightPart(),
],
),
),
),
);
}
class _LeftPart extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
hoursAndMins.format(show.start),
style: const TextStyle(...),
),
Text(
hoursAndMins.format(show.end),
style: const TextStyle(...),
),
],
);
}
}
class _RightPart extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
show.title,
style: const TextStyle(...),
),
const SizedBox(height: 4.0),
Text(show.theaterAndAuditorium),
const SizedBox(height: 8.0),
// El método de presentación esta en el lado derecho.
// Mira el widget que está abajo.
_PresentationMethodChip(),
],
),
);
}
}
class _PresentationMethodChip extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
// Chip del metodo de presentacion.
// estilizado reducido para brevedad ...
child: Text(
show.presentationMethod,
style: const TextStyle(...),
),
);
}
}
Con estos cambios, el nivel de indentación se redujo a la mitad. Ahora es fácil escanear el código de la UI y ver que está pasando.
Problema Bonus - Inventar tu propio formato de estilo
No considero esto como un problema similar a los superiores, pero aun así es algo bastante importante. ¿Por qué? vamos a ver.
Para ilustrar este problema, veamos el siguiente código:
Column(
children:[Row(children:
[Text('Hello'),Text('World'),
Text('!')])])
Esta medio extraño ¿no? Ciertamente no es algo que veas en un bueno código.
El problema: no usar dartfmt
El código superior no se apega a ninguna convención de formato común en Dart - parece que el autor de ese código invento su propio estilo. Esto no es bueno, ya que leer dicho código tomo atención extra - no maneja las convenciones a las que estamos acostumbrados.
Tener un estilo de código comúnmente acordado es esencial. Esto nos permite evitar la gimnasia mental de acostumbrarnos a un estilo extraño.
La solución: solo usa dartfmt
Por suerte, tenemos un “formateador” oficial, llamado dartfmt, que se encarga de formatear por nosotros. También, ya que hay un “monopolio de formatos”, podemos evitar discutir sobre cual formato es mejor y en su lugar concentrarnos en nuestro código.
Como una regla principal, siempre hay que poner comas después de todos los paréntesis y tener el dartfmt corriendo.
El código anterior ya formateado es mucho mas legible.
Column(
children: [
Row(
children: [
Text('Hello'),
Text('World'),
],
),
Text('!'),
],
)
Mucho mejor. Dar formato nuestro código es una obligación siempre recuerda tus comas y formatear tu código usando dartfmt.
Le agradezco mucho al autor original por permitirme traducir su articulo y espero que le sirva esta información a la comunidad de habla española.
Link al articulo original, perfil de twitter el autor y su pagina.