dart:convert es la librería fundamental para la serialización y transformación de datos en Dart. En su núcleo reside la abstracción Codec<S, T>, un objeto que define una transformación bidireccional mediante dos componentes: un encoder (que convierte de tipo S a T) y un decoder (que hace lo opuesto). Cuando solo necesitas una transformación en un solo sentido, entra en juego Converter<S, T>.
Esta separación es clave por diseño: te permite reutilizar la lógica de codificación sin obligar a implementar la decodificación si no es necesaria. Por ejemplo, json es una instancia de JsonCodec, que gestiona la complejidad de transformar mapas y listas a String y viceversa. Para manejar bytes, usamos utf8, ascii o latin1. Si necesitas combinar transformaciones, el método fuse [disponible desde Dart 2.12] es tu mejor aliado: permite encadenar dos Encoder en uno solo, como transformar un String a bytes y luego esos bytes a una representación base64.
En aplicaciones de alto rendimiento o manejo de archivos, notarás que los Converter implementan StreamTransformer. Esto te permite inyectarlos directamente en un pipeline de Stream, como cuando usas LineSplitter para procesar un archivo línea a línea sin cargar todo el contenido en memoria. Si fallas en la lógica de conversión —por ejemplo, intentando decodificar bytes que no cumplen con el esquema esperado— el runtime lanzará una FormatException, lo que puede romper tu flujo de ejecución si no se gestiona mediante el manejo de errores adecuado.
import 'dart:convert';
void main() async {
// 1. Serialización JSON y manejo de tipos
final Map<String, dynamic> userPayload = {
'id': 82,
'username': 'dart_expert',
'active': true,
'tags': ['cli', 'backend', 'dart3'],
};
// json.encode utiliza el encoder del JsonCodec
final String jsonString = json.encode(userPayload);
print('JSON Serializado: $jsonString');
// 2. Fusión de encoders: String -> UTF-8 Bytes -> Base64 Bytes
// fuse() encadena la salida de un encoder como entrada del siguiente
final encoderBase64Utf8 = utf8.encoder.fuse(base64.encoder);
final List<int> base64Bytes = encoderBase64Utf8.convert(jsonString);
// Para imprimirlo como texto legible, decodificamos esos bytes base64
print('Base64 (bytes): $base64Bytes');
print('Base64 (string): ${ascii.decode(base64Bytes)}');
// 3. Streaming: Procesamiento eficiente de líneas
// Simulamos un stream de bytes que contiene texto con distintos saltos de línea
final List<String> rawLines = [
'Primera línea\n',
'Segunda línea\r\n',
'Tercera línea (final)'
];
print('\nProcesando Stream con LineSplitter:');
final linesStream = Stream.fromIterable(rawLines.map((s) => utf8.encode(s)))
.transform(utf8.decoder) // Convierte Stream<List<int>> a Stream<String>
.transform(LineSplitter()); // Divide el stream por saltos de línea
await for (final line in linesStream) {
print('Línea recibida: $line');
}
// 4. Custom Converter: Transformación personalizada
final upperConverter = UpperCaseConverter();
print('\nCustom Converter: ${upperConverter.convert('hola dart)}');
}
/// Un Converter personalizado para transformar strings a mayúsculas.
class UpperCaseConverter extends Converter<String, String> {
@override
String convert(String input) => input.toUpperCase();
}
Desglose del código
En el primer bloque, vemos cómo json.encode simplifica el trabajo, pero por debajo está utilizando el encoder de la instancia json para transformar un Map en un String.
El segundo punto es donde la potencia de fuse se vuelve evidente. Al hacer utf8.encoder.fuse(base64.encoder), estamos creando un nuevo Converter<String, List<int>>. El flujo interno es: el String entra en el utf8.encoder, que produce una List<int> de bytes UTF-8; esos bytes pasan automáticamente al base64.encoder, que produce una nueva List<int> con la representación base64. Es una forma extremadamente eficiente de construir pipelines de transformación sin escribir loops manuales.
En el ejemplo del Stream, la potencia de dart:convert se nota en el uso de .transform(). El LineSplitter es un StreamTransformer que es inteligente: no solo busca \n, sino que gestiona correctamente la combinación de \r\n (estándar de Windows) para que el desarrollador no tenga que lidiar con caracteres fantasma al final de cada línea. Esto es vital cuando procesas archivos de logs o streams de red para evitar errores de parsing.
Finalmente, la clase UpperCaseConverter demuestra que puedes extender la funcionalidad de la librería. Al heredar de Converter<String, String>, implementas la lógica de transformación que puedes reutilizar en cualquier parte de tu aplicación, manteniendo el código limpio y declarativo.
El error frecuente
Un error común ocurre cuando intentas decodificar bytes que no coinciden con el formato esperado, especialmente con utf8.
import 'dart:convert';
void errorExample() {
// El byte 0xff no es un carácter válido en UTF-8
final invalidUtf8Bytes = [0xff, 0xd8, 0xff];
try {
// Esto lanzará una FormatException
final decoded = utf8.decoder.convert(invalidUtf8Bytes);
print(decoded);
} on FormatException catch (e) {
print('Error de decodificación: ${e.message}');
}
}
Si intentas usar utf8.decoder.convert() sobre datos binarios que no son UTF-8 (como una imagen o un archivo comprimido), la máquina virtual lanzará una FormatException. En entornos de producción, especialmente procesando datos que vienen de fuentes externas o sockets, siempre debes envolver la decodificación en un bloque try-catch o usar utf8.decoder.transform(stream) que permite manejar errores de forma más granular dentro del pipeline del stream.
N° 82