Annotations y metadatos en Dart

Las annotations (anotaciones) son metadatos que puedes adjuntar a cualquier declaración de Dart, como clases, métodos, campos o parámetros. No modifican la lógica de ejecución por sí mismas; en su lugar, funcionan como etiquetas que proporcionan información adicional a herramientas externas, al compilador o al runtime.

Para entenderlas, piensa en ellas como etiquetas adhesivas en un archivo: el texto de la etiqueta no cambia el contenido del documento, pero permite que alguien (un proceso de limpieza, un archivador o un lector) sepa exactamente qué hacer con él sin tener que leer todo el archivo.

Funcionan mediante la sintaxis @NombreDeLaAnotación o @NombreDeLaAnotación(argumentos). Internamente, Dart trata estas anotaciones como constantes que se asocian estáticamente a la estructura del código. Esto es fundamental porque la mayoría de las anotaciones se procesan en dos momentos distintos:
1. En tiempo de compilación (o pre-compilación): Herramientas como build_runner escanean tu código, detectan las anotaciones y generan código nuevo (como en json_serializable).
2. En tiempo de ejecución (Runtime): Mediante el uso de dart:mirrors (disponible principalmente en la VM de Dart para aplicaciones de consola o servidor, no en Flutter), puedes inspeccionar los metadatos de una clase mientras el programa corre.

Debes usarlas cuando necesites comunicar una intención al analizador estático (para evitar errores de diseño), cuando quieras marcar APIs obsoletas para guiar a otros desarrolladores, o cuando estés construyendo herramientas de generación de código. Si intentas usar anotaciones para controlar la lógica de negocio directamente en tiempo de ejecución (como un if basado en una anotación en un entorno AOT/Wasm), lo más probable es que tu código no se comporte como esperas, ya que la mayoría de los compiladores eliminan estos metadatos para optimizar el binario final.

Si usas mal una anotación, el riesgo principal es la falsa sensación de seguridad. Por ejemplo, usar @immutable en una clase que tiene campos no finales (non-final) no hará que el objeto sea inmutable por la fuerza; simplemente le dirá al analizador que te lance una advertencia que podrías ignorar, dejando tu arquitectura vulnerable a efectos secundarios inesperados.

import 'package:meta/meta.dart';

// 1. Definición de una anotación personalizada.
// Para que una clase sea válida como anotación, debe tener un constructor 'const'.
class DatabaseTable {
  final String name;
  const DatabaseTable(this.name);
}

// Usamos @immutable para asegurar que la configuración no cambie tras su creación.
@immutable
class UserConfig {
  @DatabaseTable('users')
  final String tableName;

  final String connectionString;

  // El constructor debe ser 'const' para que la clase sea verdaderamente inmutable.
  const UserConfig(this.tableName, this.connectionString);

  // @override es crucial para que el compilador verifique que realmente estamos 
  // sobreescribiendo el método de la clase base (Object).
  @override
  String toString() => 'UserConfig(table: $tableName)';

  // Marcamos este método como obsoleto para avisar a otros desarrolladores.
  @Deprecated('Usa el método ' 'fetchDataAsync' ' en su lugar.')
  void fetchData() {
    print('Realizando una operación síncrona (no recomendada).');
  }

  Future<void> fetchDataAsync() async {
    print('Realizando una operación asíncrona.');
  }

  // @protected indica que este método solo debe usarse por subclases.
  @Protected
  void _internalInit() {
    print('Configuración interna completada.');
  }

  // @nonVirtual le dice al compilador que no optimice este método mediante 
  // inline-expansion, asegurando que las subclases siempre puedan sobreescribirlo.
  @nonVirtual
  void logStatus() {
    print('Estado actual: Operativo');
  }
}

void main() {
  const config = UserConfig('users', 'postgres://localhost:5432');

  print(config);
  config.logStatus();
  
  // Esto disparará una advertencia del linter en el IDE.
  config.fetchData(); 
  
  // Esto es una llamada normal.
  config.fetchDataAsync();
}

Desglose del código

En el ejemplo, hemos definido una anotación personalizada llamada DatabaseTable. Fíjate que el constructor es const DatabaseTable(this.name);. Esto es obligatorio porque las anotaciones son valores constantes que se incrustan en el bytecode.

Al aplicar @immutable a UserConfig, estamos estableciendo un contrato con el analizador de Dart: “esta clase no debe tener estado mutable”. Si intentáramos añadir un método que cambie connectionString, el linter nos detendría.

La anotación @override en el método toString no es solo decorativa; si por error escribiéramos toString() (con una ‘g’ de más), el compilador nos daría un error inmediato en lugar de simplemente crear un método nuevo y dejar el toString original intacto. Esto es vital para mantener la integridad de las jerarquías de clases.

Cuando ves @Deprecated, no significa que el código no funcione, sino que el compilador y el IDE marcarán el método fetchData con una línea tachada. Es una herramienta de comunicación de diseño para la gestión del ciclo de vida de una API.

Finalmente, @nonVirtual es un detalle de optimización avanzada. En entornos de alto rendimiento, el compilador AOT podría intentar “aplanar” un método si detecta que nunca se sobreescribe, para ganar unos milisegundos. Al usar @nonVirtual, le prohibimos esa optimización, garantizando que si alguien extiende UserConfig, su implementación sea la que prevalezca.

El error frecuente
Un error muy común es intentar definir una anotación personalizada sin un constructor const.

// ERROR: Esto fallará al intentar usarlo como anotación
class BadAnnotation {
  final String value;
  BadAnnotation(this.value); // Falta el 'const'
}

// @BadAnnotation('error') // Error de compilación: "The annotation must be a constant"

Si intentas usar una clase que no sea constante como anotación, el compilador de Dart te detendrá inmediatamente. Las anotaciones deben ser evaluables en tiempo de compilación, por lo que la palabra clave const es innegociable en su definición.

109

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio