Skip to content

Instantly share code, notes, and snippets.

@psygo
Last active November 11, 2024 17:20
Show Gist options
  • Save psygo/44a661296547e4cb21c9f4b3707013cf to your computer and use it in GitHub Desktop.
Save psygo/44a661296547e4cb21c9f4b3707013cf to your computer and use it in GitHub Desktop.
Reflection and Annotations in Dart

Reflection in Dart

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.

dart:mirrors

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.

1. Basics

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: for Types — good for when working with generics.

Symbols can be called through Symbol() or simply prefixing the symbol with #.

2. Simple Reflected Call

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.

3. Creating Annotations

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 InstanceMirrors, 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.');
}

3. Refactoring a switch Statement

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.');
}

3.1 More Programmatic Calls with Named Constructors

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.');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment