pub package

flutter_aop

Annotation driven proxies that bring small, framework free AOP style hooks to Flutter and Dart code.
Mark methods with @Aop and let build_runner generate proxy classes that run your logic before, after, or when an error happens.

한국어 문서 보기

Features

  • Simple @Aop annotation with before, after, onError, and tag options.
  • Generated *.aop.dart files expose proxy classes (e.g. LoginServiceAopProxy) you can drop in wherever the original class is used.
  • Runtime AopHooks and a global AopRegistry let you attach logging, tracing, guards, or analytics without touching the original implementation.
  • Advice pointcut expressions are generated into registerWithPointcut(...) so class/method pattern matching works out of the box.
  • Built-in createObservationHooks(...) emits structured before/after/error events for logging and performance tracking.
  • Works with both synchronous and asynchronous (Future) methods and reports invocation details through an AopContext.

Getting started

Add flutter_aop to your pubspec.yaml and run flutter pub get.

dependencies:
  flutter_aop:
    path: ../flutter_aop # or use your hosted location

dev_dependencies:
  build_runner: ^2.10.4

Every library that wants AOP support must include the generated part file:

import 'package:flutter_aop/flutter_aop.dart';
part 'login_service.aop.dart';

Usage

  1. Annotate the methods that should trigger hooks.
  2. Declare aspect classes with @Aspect, @Before, @After, or @OnError and include their .aop.dart parts.
  3. Run dart run build_runner build --delete-conflicting-outputs (or flutter pub run ...) to generate both the .aop.dart files and the aggregated flutter_aop_bootstrap.g.dart.
  4. Import the generated runFlutterAopBootstrap() (e.g. import 'package:my_app/flutter_aop_bootstrap.g.dart';) and call it once during startup—this registers every proxy/aspect and internally calls ensureAllAopInitialized().
  5. Wrap your concrete instances with aopWrap(instance) (no need to touch the generated proxy class directly).
import 'package:flutter_aop/flutter_aop.dart';
import 'package:my_app/flutter_aop_bootstrap.g.dart'; // generated by build_runner
part 'login_service.aop.dart';

class LoginService {
  @Aop(tag: 'auth', description: 'track login')
  Future<void> login(String id, String password) async {
    // original implementation
  }
}

@Aspect(tag: 'auth')
class LoggingAspect {
  const LoggingAspect();

  @Before()
  void logBefore(AopContext context) =>
      print('Entering ${context.methodName} -> ${context.positionalArguments}');

  @After()
  void logAfter(AopContext context) =>
      print('Result for ${context.methodName}: ${context.result}');

  @OnError()
  void logError(AopContext context) => print('Error: ${context.error}');
}

Future<void> main() async {
  runFlutterAopBootstrap(); // call once during startup
  final service = aopWrap(LoginService());
  await service.login('gmail', '1234');
}

Need global hooks? Register them once anywhere in your app:

AopRegistry.instance.register(
  AopHooks(before: (ctx) => debugPrint('[${ctx.annotation.tag}] ${ctx.methodName}')),
  tag: 'auth',
);

Using tags keeps large projects organized—you decide which hook handles which annotated method.

Pointcuts in @Before/@After/@OnError/@Around are now generated directly. Tag merge rule:

  • effectiveTag = advice.tag ?? aspect.tag
  • If pointcut.tag is omitted, effectiveTag is injected automatically.
  • If both exist and mismatch, code generation fails early with a clear error message.

Need structured observability events without adding dependencies?

AopRegistry.instance.register(
  createObservationHooks(
    sink: (event) {
      debugPrint(
        '[${event.phase.name}] ${event.joinPointDescription} '
        'elapsed=${event.elapsed.inMilliseconds}ms slow=${event.isSlow}',
      );
    },
    slowCallThreshold: const Duration(milliseconds: 200),
  ),
  tag: 'auth',
);

Example project

The example/ directory contains a tiny console-style demo that wires hooks into a LoginService, registers aspects via GetIt/injectable, and prints every lifecycle event.

cd example
flutter pub get
dart run build_runner build --delete-conflicting-outputs
dart run lib/main.dart

Open example/lib/login_service.dart to see the annotated class, the generated example/lib/login_service.aop.dart proxy, and example/lib/flutter_aop_bootstrap.g.dart for the aggregated bootstrap file.

Dependency injection (GetIt/Injectable)

When using GetIt together with injectable, register wrapped services in a module and simply call the generated bootstrapper before getIt.init():

final getIt = GetIt.instance;

@InjectableInit()
Future<void> configureDependencies() async {
  runFlutterAopBootstrap();
  AopRegistry.instance.register(
    createObservationHooks(
      sink: (event) => debugPrint(
        '[${event.phase.name}] ${event.joinPointDescription}',
      ),
    ),
    tag: 'auth',
  );
  getIt.init();
}

@module
abstract class ServiceModule {
  @lazySingleton
  LoginService loginService() => aopWrap(LoginService());
}

The example app (example/lib/di.dart) shows the full setup, including how to resolve the proxied service from GetIt.

Generated output

Running build_runner creates *.aop.dart part files next to your source:

lib/
 ├─ login_service.dart
 └─ login_service.aop.dart  <-- generated

Every class with at least one @Aop method receives a proxy and registers itself inside AopProxyRegistry. You rarely need the proxy class directly—just call aopWrap(Service()) (or AopProxyRegistry.instance.wrap(Service(), hooks: ...)) to receive the instrumented instance. All generated bootstrap functions are aggregated into flutter_aop_bootstrap.g.dart; call runFlutterAopBootstrap() to execute them once and ensureAllAopInitialized() is invoked automatically at the end.

Tips

  • Hooks for synchronous methods must be synchronous too. If you need async work (e.g. writing to storage), mark the original method async so the proxy can await your hook.
  • positionalArguments and namedArguments inside AopContext give you the exact values passed to the method.
  • The optional description field is never used by the runtime but is emitted in generated comments—handy when you read the .aop.dart file.
  • You can short-circuit the original call with context.skipWithResult(...) in a before hook (e.g. return a cached value). OnError hooks can recover by clearing context.error and setting a replacement context.result.

Running the generator

dart run build_runner build --delete-conflicting-outputs

Use watch while developing to regenerate proxies/bootstraps when files change:

dart run build_runner watch

Contributing

Issues and ideas are welcome! File bugs or feature requests in the repository issue tracker. If you send a PR, please include tests (flutter test) and run flutter pub run build_runner build so the generated code stays in sync.

Libraries

builder
flutter_aop