Por qué usamos código generado en nuestra Flutter App

Carlos Daniel
Building La Haus
Published in
6 min readJun 29, 2021
Photo by Karsten Würth on Unsplash

Cuando desarrollamos apps, parte de las abstracciones que hacemos, es que debemos garantizar que los valores de nuestros objetos siempre conservarán el mismo valor para todo, es decir, que cuando modelamos una abstracción (ej. Objeto User) su valor nunca cambiará y se mantendrá en el tiempo, siguiendo el principio de que por naturaleza sus value types no variarán.

Un ejemplo

Imaginemos tener un user1 y un user2, ambos del tipo User, y creando instancias para ellos, con el mismo nombre:

final user1 = new User(name: 'Carlos D');
final user2 = new User(name: 'Carlos D');
print(user1 == user2);

Qué creen que se imprime? true o false?

print(user1 == user2) imprime false

El punto es que creemos que los objetos son los mismos, pero realmente no lo son, y tenemos que sobreescribir una cantidad gigante de código para confirmar que su hashCode sea diferente, lo mismo para el toString(), o nuestros custom equals, y que a su vez debemos unit testear. De esto se desprende que tendremos mucho más código para mantener, pero la premisa es que mientras menos código para mantener, muchísimo mejor!

Adicional, si para el ejemplo anterior, en cualquier momento hiciera algo como:

user1.age = 33

Qué pasa si alguien tiene una referencia de user2 (que se “supone” es el mismo que user1) pero no recibió esa actualización de la edad? La respuesta es simple: Tendría data desincronizada.

Acerca de inmutabilidad (resumido)

Bajo el concepto más simple: algo es inmutable cuando no se puede modificar. Una variable es inmutable cuando su valor no se puede modificar después de haberse inicializado. Y un objeto lo es cuando su estado no puede ser actualizado tras su creación.

No profundizaremos mucho más en la descripción de inmutabilidad y sus variantes, así que les comparto una referencia con todo lujo de detalles, pero por ahora miremos cómo aplicarla a un contexto bien particular.

Getty Images

Inmutabilidad aplicada

En Kotlin, el lenguaje mismo garantiza inmutabilidad de los atributos de un objeto según su definición como val, pero y en Dart? … Bueno, Dart no ofrece este feature, pero sí ofrece la posibilidad de hacer algo vía metaprogramming a través de source_gen (que no es más que un set de utilidades en Dart para la generación automática de código).

Entonces el objetivo es simple, facilitar la definición y uso de los value types garantizando inmutabilidad pero con menos boilerplate.

Ahora, los detalles y explicación de implementación del source_gen no serán el alcance de este escrito (les dejaré compartidas referencias afines, entre ellas esta) pero lo más importante es que los casos de uso para nuestro escenario. Esto nos ha permitido de una forma simple y flexible poder garantizar no solo inmutabilidad de los valores de nuestros objetos, lo que a la postre minimiza los issues por datos corruptos en nuestra app, sino que adicionalmente podemos extender su uso para una adecuada serialización (obviamente, con menos código) además que nos evita tener más código para mantener (imagínense un equals == por ahí suelto sin testear 😬).

Aquí es donde entran en acción paquetes como built_value y built_collection.

Lo anterior, nos lleva entonces a ser eficientes definiendo nuestras clases, y dejando “el resto de responsabilidad” al generador de código, peeeero, lo más importante, lo hará bajo nuestra directriz, pues aprovechamos las bondades de Dart haciendo uso del cascade operator y de un buen diseño asumiendo un builder type para el constructor (con una función que toma un builder como parámetro):

Me refiero a algo como esto:

// como el source gen necesita espacio para sus implementaciones, nuestra clase usuario debe ser abstractabstract class User {
factory User([updates(UserBuilder b)]) = _$User;

String get name;
int get age;
User._();
}
//esto nos lleva entonces a:
final user1 = User((b) => b
..name = 'Carlos D'
..age = 33);

Lo anterior nos deja ver 2 cosas importantes:
1. La flexibilidad y facilidad de definir un objeto con valores inmutables.
2. La simpleza de instanciar el objeto.

Ahora, si necesitamos crear nuevos valores basados en los anteriores, entonces hacemos un rebuilddel objeto, no creamos nuevas instancias, lo cual nos garantiza unicidad y calidad de la información:

user1 = user1.rebuild((b) => b..age = 35);

… Y dónde está definido el rebuild?

En el paquete built_value. Por ende, esa responsabilidad de generar todo ese código verboso (pero no menos importante) recae en él. Así la implementación final de nuestro objeto User:

import 'package:built_value/built_value.dart';//por convención el generado debe llamarse igual que el fuente pero //con el .g
part 'user.g.dart';
abstract class User implements Built<User, UserBuilder> {
factory User([updates(UserBuilder b)]) = _$User;
String get name;
int get age;

User._();
}

Comentarios finales

Este es nuestro caso de uso principal para la auto generación de código en nuestros objetos, pero resaltando que entendemos adecuadamente el por qué usamos built_value, además de saber que la implementación generada de código no tiene un alcance solo de los campos/atributos de nuestras clases sino también que provee el operador ==, hasCode, toString y validación del nullpara campos obligatorios, que hacen parte de los cuestionamientos iniciales de los primeros párrafos del artículo.

Adicional a esto último, jugando con los internals de la librería, nos damos cuenta que al actualizar los valores, ésta solo copia los valores nuevos, dejando los demás quietos y solo reusándolos, lo cual influye en su rapidez y eficiencia en memoria.

Nota: Ojo! Hoy built_valuenos suple esta necesidad que tenemos de autogeneración de código, inmutabilidad, etc., pero no quiere decir que nos casemos ciegamente con la librería. Reconocemos su facilidad de uso, eficiencia y el gran soporte de la misma, pero mañana, no sabremos qué pueda pasar. Lo importante, entender por qué está ahí y el valor generado.

Just to check 🤓

Después de haber generado la clase User a través de built_value entonces qué creen que va a imprimir: true o false?

final user1 = User((b) => b
..name = 'Carlos D'
..age = 36);
final user2 = User((b) => b
..name = 'Carlos D'
..age = 36);
print(user1 == user2);

.. y acaso todo es bueno con built_value?

Resulta que no, que ganamos muchas cosas positivas e interesantes ya mencionadas, pero su uso también tiene sus desventajas. Quizás la más visible es que el tiempo de hacer build del proyecto puede aumentar porque primero tiene que autogenerar, y eso toma tiempo. Además, si tenemos que cambiar de ramas que están en diferentes estados de desarrollo del proyecto muchas veces, tenemos que limpiar y volver a generar todo para correr el proyecto. lo cual crear una fricción, pero de momento es manejable. Pero a hoy, en nuestro escenario, los beneficios superan los inconvenientes, pero es un tema que no debe dejar de tenerse en cuenta y evaluarse periódicamente.

Referencias

--

--

Carlos Daniel
Building La Haus

Android & Flutter Developer. GDE for Android & Mobile Engineer.