loading...

Freezed - Data Class y Union en un package de Dart

acanteror profile image Antonio Cantero Ruiz ・8 min read

Este tutorial es una traducción del genial trabajo de Matej Rešetár. Puedes leer la versión original aquí o si lo prefieres ver el videotutorial.

¿Qué te parecería si Dart tuviera data classes y sealed classes como las conocemos de Kotlin? Sería perfecto pero de momento, aunque se trabaja en estas características, todavía están en un futuro lejano. Pero ahora tenemos lo siguiente: estamos a punto de ser congelados con el nuevo paquete de Rémi Rousselet, el creador de provider.

Pero ya hay un paquete para eso…

Si has estado siguiendo la evolución de Dart y su ecosistema de packages por un tiempo, podrías estar pensando "Tenemos sealed_unions, super_enum, sum_types… ¿Por qué necesitamos otro paquete más?". (Sí, todos esos paquetes han sido cubiertos por tutoriales en resocoder.com 😉) De manera similar, si eres un ávido seguidor de Dart, built_value, equatable e incluso ciertas extensiones de VS Code pueden venirte a la cabeza. Entonces, ¿por qué otra vez otro paquete?

Podría hablar durante bastante tiempo sobre las insuficiencias de los paquetes anteriormente mencionados, así que seré breve. Son demasiado verbosos, demasiado restrictivos o demasiado feos para usarlos a diario. Esa es una razón en sí misma para cambiar a freezed, lo que proporciona una sintaxis sucinta y elegante.

¿Todavía no te he convencido? Bueno, ¡freezed puede ser usado tanto para data classes como para uniones! Esto significa que obtendrá automáticamente la igualdad de valores generados, copyWith, switch exhaustivo, e incluso soporte de serialización de JSON desde un solo lugar! Básicamente obtienes built_value y sum_types sin todas las rarezas y código repetitivo.

Configurando el proyecto

Aunque para este tutorial usaremos una aplicación de consola, todo lo que trabajemos se puede aplicar también a Flutter. Primero, añadiremos los paquetes necesarios a pubspec.yaml. Además de freezed, también vamos a añadir json_serializable ya que está muy bien integrado.

///pubspec.yaml

dependencies:
  json_annotation: ^3.0.1
  meta: ^1.1.8

dev_dependencies:
  build_runner:
  freezed: ^0.1.3+1
  json_serializable: ^3.2.5

Creando una data class

En caso de que no estés familiarizado con el término data class, es simplemente una clase con igualdad de valor, método copyWith, sus campos son inmutables y normalmente soporta fácilmente la serialización. Además, si estás familiarizado con Kotlin, sabes que puedes reducir considerablemente el código definiendo los campos directamente en el constructor de esta manera:

//kotlin_data_class.kt

data class User(val name: String, val age: Int)

Compara eso con la cantidad de código que se necesita con un Dart puro:

///dart_regular_data_class.dart

@immutable
class User {
  final String name;
  final int age;
  User(this.name, this.age);
  User copyWith({
    String name,
    int age,
  }) {
    return User(
      name ?? this.name,
      age ?? this.age,
    );
  }

  @override
  bool operator ==(Object o) {
    if (identical(this, o)) return true;
    return o is User && o.name == name && o.age == age;
  }

  @override
  int get hashCode => name.hashCode ^ age.hashCode;
}

Las cosas no van tan bien para Dart hasta ahora. Por supuesto, puedes ir con built_value pero entonces tendrás que lidiar con su extraña sintaxis. Pero no temas porque la simplicidad de freezed está a sólo unas pocas teclas.  Comencemos creando un nuevo archivo freezed_classes.dart.

///freezed_classes.dart

import 'package:meta/meta.dart';
part 'freezed_classes.freezed.dart';
@immutable
abstract class User with _$User {
  const factory User(String name, int age) = _User;
}

Vamos a crear todas las clases de freezed dentro de este archivo para no tener que especificar el import y las declaraciones part una y otra vez.

No está mal, ¿verdad? Es verdad que todavía hay una cierta cantidad de código repetitivo, pero es bastante estándar y mínima. Después de ejecutar el comando favorito de cada desarrollador de Flutter...

flutter pub run build_runner watch —delete-conflicting-outputs

Ahora podemos usar nuestra data class User con todas sus ventajas y sacar partido de la inmutabilidad, igualdad de valores y copyWith

///main.dart

void main() {
  final user = User('Matt', 20);
  // user.age = 5; // error, User is immutable
  final user2 = user.copyWith(name: 'John');
  final sameValueUser1 = User('abc', 123);
  final sameValueUser2 = User('abc', 123);
  print(sameValueUser1 == sameValueUser2); // true
  print(user); // User(name: Matt, age: 20)
}

Los parámetros del constructor de nuestra data class User actualmente son posicionales. Sin embargo, no tiene por qué ser así, ya que podemos hacerlos opcionales o nombrados.

///freezed_classes.dart

@immutable
abstract class User with _$User {
  const factory User(String name, {int age}) = _User;
}

Añadiendo serialización JSON

La serialización de datos hacia y desde JSON es muy simple con json_serializable y, afortunadamente, freezed fue construido para trabajar bien con él! ¡No más serialización personalizada y extraña como con built_value! Como ya lo hemos añadido como una dependencia, sólo tenemos que añadir la pequeña plantilla necesaria para que el generador de json_serializable funcione.

Asegúrate de importar json_annotation.dart y declarar la sentencia part '*.g.dart'. Fíjate en que sólo definimos el método fromJson y que no anotamos la clase con @JsonSerializable.

///freezed_classes.dart

import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';

part 'freezed_classes.freezed.dart';
part 'freezed_classes.g.dart';

@immutable
abstract class User with _$User {
  const factory User(String name, int age) = _User;
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}

Esto generará todo el código necesario para llamar a toJson como un método de instancia y a fromJson como un método factory

///main.dart

void main() {
  final user = User('Matt', 20);
  final Map<String, dynamic> serialized = user.toJson();
  final User deserialized = User.fromJson(serialized);
}

La serialización también está plenamente integrada con las unions class, lo cual es genial 😎 Más sobre ello a continuación.

Crear una union/sealed class

Las data classes son ya un motivo suficiente para usar freezed pero, además, la elegancia con la que se pueden crear unions es simplemente 👌 magnífica. Si has usado cualquier otro paquete seguro que conoces su extraña sintaxis y sus limitaciones. Echemos un vistazo a la gracia de las sealed classes de Kotlin:

///kotlin_sealed_classes.kt

// Nest classes to access them only as Operation.Add or 

Operation.Subtract
sealed class Operation {
  class Add(val value: Int) : Operation()
  class Substract(val value: Int) : Operation()
}

// Don't nest classes if you want to instantiate Add or Subtract directly
sealed class Operation
class Add(val value: Int) : Operation()
class Substract(val value: Int) : Operation()
fun main() {
  ...
  // exhaustive "switch", notifies you if you forget to handle a case
  when(operation) {
    is Add -> //do something
    is Subtract -> // do something
  }
}

Aunque no sepas nada de Kotlin, deberías entender lo que está pasando. ¿Podemos replicar esto con freezed? ¡Claro que podemos! A diferencia de otros paquetes, freezed permite incluso el paradigma anidado/no anidado. Veamos qué es posible.

Sealed classes “anidadas”

La belleza de las sealed classes anidadas en Kotlin es que no puedes instanciar las subclases individuales individualmente. Con freezed, esto significa que escribir final operation final = Add(); es inválido mientras que escribir final operation final = Operation.add() es válido. ¿Cómo podemos lograr tal cosa? Es muy fácil. Sólo hay que hacer que la clase generada para el caso de la unión sea privada.

///freezed_classes.dart

@immutable
abstract class OperationNested with _$OperationNested {
  // "Nested" unions have private generated classes (underscore)
  const factory OperationNested.add(int value) = _Add;
  const factory OperationNested.subtract(int value) = _Subtract;
}

Entonces podemos instanciar un caso de unión llamando al método factory y usar el método when para hacer un switch exhaustivo con todos los casos posibles.

///main.dart

void main() {
  final result = performOperation(2, OperationNested.add(2));
  print(result); // 4
}

// Function pretending to do something useful
int performOperation(int operand, OperationNested operation) {
  // Like switch statement but forgetting about a case will result in info/error
  return operation.when(
    add: (value) => operand + value,
    subtract: (value) => operand - value,
  );
}

Los parámetros del método when son @required. Sin embargo, el Dart sólo te da un mensaje "info" no fatal por defecto. Para cambiar este mensaje por uno de error, habilita las reglas personalizadas del linter.

Sealed classes "no anidadas"

A veces es agradable no tener que llamar a los constructores factory sino instanciar directamente las clases. Esto puede ser útil con los eventos y estados de la un BLoC, por ejemplo. Lograr esto es tan simple como hacer públicas las clases generadas.

///freezed_classes.dart

@immutable
abstract class OperationNonNested with _$OperationNonNested {
  // "Non-nested" unions have public generated classes (no underscore)
  const factory OperationNonNested.add(int value) = Add;
  const factory OperationNonNested.subtract(int value) = Subtract;
}

Esto permite dos formas de instanciación: usar el método factory o instanciar la clase directamente.

///main.dart

void main() {
  // Still possible to use the factory
  final result1 = performOperation(2, OperationNonNested.add(2));
  // But non-nested union cases can also be instantiated directly
  final result2 = performOperation(2, Add(2))
  print(result1); // 4
  print(result2); // 4
}
int performOperation(int operand, OperationNonNested operation) {
  // When method still works even with cases instantiated directly
  return operation.when(
    add: (value) => operand + value,
    subtract: (value) => operand - value,
  );
}

Otros métodos switch

Hasta ahora hemos visto el método when pero hay 3 métodos más. Está el no exhaustivo maybeWhen que permite ignorar ciertos casos de unión y proporcionar una función orElse para ejecutar en su lugar.

///main.dart

return operation.maybeWhen(
  add: (value) => operand + value,
  // ignoring subtract
  orElse: () => -1,
);

Luego está el método map y su respectivo mabeMap. Son muy similares a when y maybeWhen pero en lugar de pasar el valor desestructurado (int en el caso de nuestra unión de ejemplo), pasan la clase que contiene los datos (Add o Subtract en nuestro ejemplo).

///main.dart

int performOperation(int operand, OperationNonNested operation) {
  return operation.map(
    add: (Add caseClass) => operand + caseClass.value,
    subtract: (Subtract caseClass) => operand - caseClass.value,
  );
}

Tener acceso a la clase puede ser útil cuando, por ejemplo, quieres llamar a copyWith en ella o si estás implementando un widget de BlocBuilder en el que quieres mapear los estados entrantes.

BONO: Snippets de VS Code

El código que necesitas para usar freezed es mínimo, pero sigue ahí. Por tanto seguro que encontrarás estos snippets útiles.  Sólo tienes que pulsar F1 o Ctrl/Cmd + Mayúsculas + P, introducir "Configurar fragmentos de usuario" y editar el archivo dart.json.

{
  ...
  "Part statement": {
    "prefix": "pts",
    "body": [
      "part '${TM_FILENAME_BASE}.g.dart';",
    ],
    "description": "Creates a filled-in part statement"
  },
  "Part 'Freezed' statement": {
    "prefix": "ptf",
    "body": [
      "part '${TM_FILENAME_BASE}.freezed.dart';",
    ],
    "description": "Creates a filled-in freezed part statement"
  },
  "Freezed Data Class": {
    "prefix": "fdataclass",
    "body": [
      "@immutable",
      "abstract class ${1:DataClass} with _$${1:DataClass}{",
      "  const factory ${1:DataClass}(${2}) = _${1:DataClass};",
      "}"
    ],
    "description": "Freezed Data Class"
  },
  "Freezed Union": {
    "prefix": "funion",
    "body": [
      "@immutable",
      "abstract class ${1:Union} with _$${1:Union}{",
      "  const factory ${1:Union}.${2}(${4}) = ${3};",
      "}"
    ],
    "description": "Freezed Union"
  },
  "Freezed Union Case": {
    "prefix": "funioncase",
    "body": [
      "const factory ${1:Union}.${2}(${4}) = ${3};"
    ],
    "description": "Freezed Union Case"
  },
  ...
}

¡Muestra algo de apoyo a Rémi dándole a este paquete un like en pub.dev y una estrella en GitHub! Por fin tenemos una solución de datos/sealed class que no compromete nada.

Posted on by:

acanteror profile

Antonio Cantero Ruiz

@acanteror

Andaluz por parte de padres y mexicano por parte de hija. Enseñar para aprender, no hay más.

Discussion

markdown guide
 

hola Antonio esta muy genial tu articulo necesito que me ayudes ocn algo de freezed ... tengo el siguiente json:
{
"page": 1,
"per_page": 3,
"total": 12,
"total_pages": 4,
"author": {
"first_name": "Ms R",
"last_name": "Reddy"
},
"data": [
{
"id": 1,
"first_name": "George",
"last_name": "Bluth",
"avatar": "s3.amazonaws.com/uifaces/faces/twi...",
"images": [
{
"id": 122,
"imageName": "377cjsahdh388.jpeg"
},
{
"id": 152,
"imageName": "1743fsahdh388.jpeg"
}
]
}
}

como puedo usar freezed aqui ya obtengo la parte de arriba pero la de data me da un error porque es un list de data como lo puedo hacer ocn freezed?

 

Hola, por el json yo diría que data podría ser un array de objetos user, y cada user a su vez tiene un array de objetos image.
Tienes herramientas como app.quicktype.io/ que te pueden ayudar a generar los modelos a partir de un json, aunque luego tendrás que simplificarlo para adaptarlo a la data class, pero te puede servir de orientación.

 

final page = Page(56, 156, 256, 358, Author('Nestor', 'Marquez'));

Asi lo uso en le main para ir probandolo pero cuando agrego data o IMage me dice que no s epued eporque son list alli es donde tengo la duda

Si es un list, tendrás un List, por ejemplo en el type del param

 

@immutable
abstract class Page with _$Page {
const factory Page(
int page, int perPage, int total, int totalPages, Author author) = _Page;

factory Page.fromJson(Map json) => _$PageFromJson(json);
}

@immutable
abstract class Author with _$Author {
const factory Author(String name, String lastName) = _Author;

factory Author.fromJson(Map json) => _$AuthorFromJson(json);
}

@immutable
abstract class Data with _$Data {
const factory Data(
int id,
String firstName,
String lastName,
String avatar,
) = _Data;

factory Data.fromJson(Map json) => _$DataFromJson(json);
}

@immutable
abstract class Image with _$Image {
const factory Image(int id, String imageName) = _Image;

factory Image.fromJson(Map json) => _$ImageFromJson(json);
}
asi lo separe pero no se como traerlo en el class Page

Ok, ya tienes las clases, ya solo te queda pasarlo a data class. Es sencillo.
Para la modelo Image sería algo así:
///image_model.dart
import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';

part 'image_model.freezed.dart';
part 'image_model.g.dart'; @immutable

abstract class Image with _$Image {
const factory Image(int id, String imageName) = _Image;
factory Image.fromJson(Map json) => _$ImageFromJson(json);
}
Y el generador de código hará el resto.

Yo pondría cada modelo en un archivo independiente.

ok entiendo eso ... pero como podria instanciarlos en la clase page?

image va dentro de data y data a su vez va dentro de page ... no se si me doy a entender

No tienes que instanciarlos.
Cuando parsees un json con ese schema ya se crearán todos los modelos necesarios.
El uso es igual que sin data class.

ok creo las clases separadas y en page solo coloco
List data;

Asi no mas?

flutter-es.io/docs/cookbook/networ...
Creo que te será útil echarle un vistazo a la doc básica antes de usar data classes.
El paso de model a data class es sencillo, pero es necesario tener claros algunos conceptos previos.

siempre me dan guerra los List yo ya e visto eso solo queria usarlos con freezed

hice lo que sugeriste separe todas las clases pero aun obtengo errores me dice algo asi
The argument type 'Image' can't be assigned to the parameter type 'List'

asi tengo la clase data
@immutable
abstract class Data with _$Data {
const factory Data(
int id,
String firstName,
String lastName,
String avatar,
List imageList,
) = _Data;

factory Data.fromJson(Map json) => _$DataFromJson(json);
}

que estoy haciendo mal? no comprendo

Pásame un link a un repo en donde tengas tú código y le echo un vistazo
No sería List ?