Unfortunately, since mirrors currently bloat compiled code, it isn't supported for Flutter or web apps. A workaround would be to use a package like
source_gen
.
Most of the info here, comes from Understanding Reflection and Annotations in Dart tutorial, from the Fullstack Dart and Flutter Tutorials YouTube channel. He also created a gist with it.
There are basically 3 functions which let you access metadata on your program:
reflect
: for instances of objects (reflectee) — the one you're most likely going to use the most.reflectClass
: for classes.reflectType
: forType
s — good for when working with generics.
Symbols can be called through Symbol()
or simply prefixing the symbol with #
.
import 'dart:mirrors' show InstanceMirror, reflect;
void main(List<String> arguments) {
final InstanceMirror ref = reflect(Endpoint());
ref.invoke(#handle, []);
print(ref.type.instanceMembers);
}
class Endpoint {
final String field = '';
void handle() => print('Request received');
}
The invoke
method can also handle optional and named parameters.
Annotations are simple Dart classes with const
constructors used in tandem with reflection.
Use the metadata
property to extract the annotation's data. Metadata is a list of InstanceMirror
s, so you could potentially use multiple ones at the same time — and use list methods to filter the annotation you want by type.
import 'dart:io' show HttpServer, HttpRequest;
import 'dart:mirrors' show InstanceMirror, reflect;
Future<void> main() async {
final HttpServer server = await HttpServer.bind('localhost', 8080);
await for (HttpRequest req in server) {
final InstanceMirror reflectedEndpoint = reflect(Endpoint());
final String path = req.uri.path;
final Route routeAnnotation = reflectedEndpoint.type.metadata
.firstWhere((InstanceMirror metadata) => metadata.reflectee is Route,
orElse: () => null)
.reflectee;
if (routeAnnotation != null && path == routeAnnotation.url)
reflectedEndpoint.invoke(#handle, []);
await req.response.close();
}
}
class Route {
final String url;
const Route(this.url);
}
@Route('/')
class Endpoint {
void handle() => print('Request received.');
}
Originally, you could have handled different incoming requests like this:
@Route('/')
class Endpoint {
final HttpRequest _req;
Endpoint(this._req);
void handle() {
switch (_req.method) {
case 'GET':
_handleGet();
break;
case 'POST':
_handlePost();
break;
case 'PUT':
_handlePut();
break;
case 'DELETE':
_handleDelete();
break;
}
}
void _handleGet() => _req.response.write('Got GET request.');
void _handlePost() => _req.response.write('Got POST request.');
void _handlePut() => _req.response.write('Got PUT request.');
void _handleDelete() => _req.response.write('Got DELETE request.');
}
With reflection, you will be able to get something like this:
import 'dart:io' show HttpServer, HttpRequest;
import 'dart:mirrors' show InstanceMirror, MethodMirror, reflect;
Future<void> main() async {
final HttpServer server = await HttpServer.bind('localhost', 8080);
await for (HttpRequest req in server) {
final InstanceMirror reflectedEndpoint = reflect(Endpoint(req));
final String path = req.uri.path;
final Route routeAnnotation = reflectedEndpoint.type.metadata
.firstWhere((InstanceMirror metadata) => metadata.reflectee is Route,
orElse: () => null)
.reflectee;
if (routeAnnotation != null && path == routeAnnotation.url) {
reflectedEndpoint.invoke(#handle, []);
reflectedEndpoint.type.instanceMembers
.forEach((_, MethodMirror methodMirror) {
if (methodMirror.isOperator ||
!methodMirror.isRegularMethod ||
methodMirror.owner.simpleName != #Endpoint) return;
final InstanceMirror routeMethod = methodMirror.metadata.firstWhere(
(InstanceMirror instanceMirror) =>
instanceMirror.reflectee is RouteMethod,
orElse: () => null);
if (routeMethod != null && (routeMethod.reflectee as RouteMethod).method == req.method) {
reflectedEndpoint.invoke(methodMirror.simpleName, []);
}
});
}
await req.response.close();
}
}
class Route {
final String url;
const Route(this.url);
}
class RouteMethod {
final String method;
const RouteMethod(this.method);
}
@Route('/')
class Endpoint {
final HttpRequest _req;
Endpoint(this._req);
@RouteMethod('GET')
void handleGet() => _req.response.write('Got GET request.');
@RouteMethod('POST')
void handlePost() => _req.response.write('Got POST request.');
@RouteMethod('PUT')
void handlePut() => _req.response.write('Got PUT request.');
@RouteMethod('DELETE')
void handleDelete() => _req.response.write('Got DELETE request.');
}
Static analysis and exception handling might benefit from using named constructors instead of one constructor with strings.
class RouteMethod {
final String method;
const RouteMethod(this.method);
const RouteMethod.get() : this('GET');
const RouteMethod.post() : this('POST');
const RouteMethod.put() : this('PUT');
const RouteMethod.delete() : this('DELETE');
}
@Route('/')
class Endpoint {
final HttpRequest _req;
Endpoint(this._req);
@RouteMethod.get()
void handleGet() => _req.response.write('Got GET request.');
@RouteMethod.post()
void handlePost() => _req.response.write('Got POST request.');
@RouteMethod.put()
void handlePut() => _req.response.write('Got PUT request.');
@RouteMethod.delete()
void handleDelete() => _req.response.write('Got DELETE request.');
}