noSuchMethod es el mecanismo de “escape” de la máquina virtual de Dart cuando intentas acceder a un miembro que no existe en la jerarquía de clases de un objeto. En lugar de lanzar inmediatamente un NoSuchMethodError, la VM busca este método en la instancia. Si está implementado, la ejecución se desvía hacia él, recibiendo un objeto Invocation que encapsula todo el contexto de la llamada: el nombre del miembro (memberName como Symbol), si se trata de un getter, un setter o un método, y sus argumentos (tanto posicionales como nombrados).
Este método es la pieza fundamental para implementar patrones como el Proxy o los Mocks de forma dinámica. Por diseño, la VM permite que un objeto responda a mensajes que no están declarados en su código fuente, delegando esa responsabilidad al desarrollador. Para que el analizador estático de Dart te permita tratar a una clase como si implementara una interfaz sin tener que escribir todos sus métodos manualmente, debes declarar la firma como dynamic noSuchMethod(Invocation invocation). Esto engaña al compilador, indicándole que la clase es capaz de responder a cualquier contrato.
Debes usarlo cuando necesites construir objetos que actúen como intermediarios (proxies) o cuando estés desarrollando herramientas de testing que generen mocks en tiempo de ejecución. Si intentas usarlo para ocultar errores de diseño en tu lógica de negocio, estarás sacrificando la seguridad del sistema de tipos. Si implementas noSuchMethod pero no manejas correctamente los tipos de retorno, lo que parece un error de compilación se transformará en un error de tipo Runtime muy difícil de rastrear, especialmente en entornos AOT donde la depuración es más estricta.
import 'dart:core';
/// Una interfaz de base para nuestro ejemplo.
abstract class DataRepository {
Future<String> fetchUser(int id);
String get connectionName;
void logAction(String action, {required String severity});
}
/// Un Mock que no implementa explícitamente los métodos de DataRepository,
/// pero "engaña" al compilador para que crea que lo hace.
class MockRepository {
// Al declarar noSuchMethod como 'dynamic', el analizador nos permite
// tratar esta clase como si fuera de cualquier tipo mediante un cast.
@override
dynamic noSuchMethod(Invocation invocation) {
// Identificamos el método mediante el Symbol
switch (invocation.memberName) {
case #fetchUser:
// Extraemos argumentos posicionales
final id = invocation.positionalArguments.first as int;
print('Mock: Simulando fetchUser para ID $id');
return Future.value('Usuario de prueba #$id');
case #connectionName:
// Interceptamos un getter
return 'In-Memory-Mock-DB';
case #logAction:
// Interceptamos un método con argumentos nombrados
final action = invocation.positionalArguments.first;
final severity = invocation.namedArguments[#severity];
print('Mock Log [$severity]: $action');
return null;
default:
// Si el método no es uno que nos interesa, lanzamos el error estándar
throw NoSuchMethodError.noSuchMethod(
this,
invocation.memberName,
'Método no implementado en MockRepository',
);
}
}
}
void main() async {
// Usamos 'as DataRepository' para tratar al mock como la interfaz real.
// Esto funciona gracias a la firma 'dynamic' en noSuchMethod.
final repo = MockRepository() as DataRepository;
print('--- Iniciando ejecución ---');
// Caso 1: Llamada a método asíncrono
final user = await repo.fetchUser(101);
print('Resultado: $user');
// Caso 2: Acceso a getter
print('Conectado a: ${repo.connectionName}');
// Caso 3: Método con argumentos nombrados
repo.logAction('Consulta realizada', severity: 'LOW');
print('--- Fin de la ejecución ---');
}
Desglose del concepto
En el ejemplo anterior, la clase MockRepository no tiene implementado el método fetchUser, connectionName ni logAction. Sin embargo, al usar as DataRepository, el compilador confía en que el objeto responderá a esos mensajes.
El corazón de la magia es invocation.memberName. En Dart, los nombres de los métodos se convierten en Symbol (como #fetchUser). Esto nos permite usar un switch para decidir qué lógica ejecutar según el método llamado. En el caso de fetchUser, accedemos a invocation.positionalArguments, que es una List<dynamic>, para obtener el id.
Fíjate en la importancia del tipo de retorno en noSuchMethod. Como la firma es dynamic, podemos devolver un Future.value(...) para el método fetchUser y un String para el getter connectionName. Si hubiéramos declarado el método como void, el programa fallaría al intentar usar el resultado de fetchUser en un await, ya que el runtime intentaría tratar un Future inexistente como un objeto válido.
Cuando llamamos a repo.logAction, el mapa namedArguments contiene los parámetros que pasamos con la sintaxis key: value. Es vital usar el Symbol correspondiente (en este caso #severity) para recuperar el valor, de lo contrario, el mapa no encontrará la clave.
El error frecuente
Un error crítico ocurre al desajustar la naturaleza asíncrona de los métodos originales. Si el método original es Future<String> fetchUser(...), pero en tu noSuchMethod devuelves simplemente un String (sin envolverlo en un Future), el código compilará porque el tipo de retorno de noSuchMethod es dynamic. Sin embargo, al hacer await repo.fetchUser(101), el runtime intentará tratar ese String como un Future.
// ERROR SUTIL
@override
dynamic noSuchMethod(Invocation invocation) {
if (invocation.memberName == #fetchUser) {
return 'Soy un string, no un Future'; // Esto romperá el await en el caller
}
}
// El error ocurrirá aquí:
// Un `String` no tiene el método `.then()` que espera el `await`
await repo.fetchUser(1);
Este error suele lanzarse como un NoSuchMethodError: type 'String' has no method 'then', lo cual puede confundir si solo estás mirando la lógica interna del mock y no la firma del método original.
N° 108