Skip to main content

spring_core_dart 0.1.3 copy "spring_core_dart: ^0.1.3" to clipboard
spring_core_dart: ^0.1.3 copied to clipboard

Spring Core for Dart and Flutter: annotation-based dependency injection, application.properties configuration, profiles, events, SpEL, validation and AOP.

spring_core_dart #

Spring Core for Dart: IoC container, environment and application.properties configuration, events, messages, resources, type conversion, SpEL, validation with data binding, and AOP. Pure Dart, works in Flutter (no dart:mirrors): annotations are processed at build time by spring_core_dart_generator.

Repository: https://github.com/farum-azula/spring_core_dart. Flutter widgets (bean builders, event listeners in the widget tree) will be a separate library.

Packages #

Package Path What it is
spring_core_dart spring_core_dart/ Runtime: annotations, container, environment, events, SpEL, validation, AOP. No dependencies.
spring_core_dart_generator spring_core_dart_generator/ build_runner builder that turns annotations into *.beans.dart modules.
spring_core_dart_example example/ Every feature in use, with end-to-end tests over the generated code.

Libraries of the runtime (import one, or package:spring_core_dart/spring_core_dart.dart for all):

Library Spring counterpart
core.dart spring-core: Environment, property sources, profiles, ConversionService, resources, AntPathMatcher, Spring Boot config data
beans.dart spring-beans: BeanDefinition, DefaultListableBeanFactory, FactoryBean, post-processors, scopes, BeanWrapper, introspection
context.dart spring-context: annotations, ApplicationContext, conditions, events, lifecycle, MessageSource
expression.dart spring-expression (SpEL)
validation.dart org.springframework.validation + Bean Validation style constraints
aop.dart spring-aop: aspects, advice, pointcut expressions, advisors

Installation #

dependencies:
  spring_core_dart: ^0.1.0

dev_dependencies:
  build_runner: ^2.17.0
  spring_core_dart_generator: ^0.1.0

Generator option (in your build.yaml):

targets:
  $default:
    builders:
      spring_core_dart_generator:spring_core_dart:
        options:
          aop_proxies: all   # all (default) | interfaces | none

Quick start #

# config/application.properties
app.title=NEON
spring.profiles.active=dev
// lib/src/beans.dart — everything under lib/src/ is scanned
import 'package:spring_core_dart/spring_core_dart.dart';
import 'package:my_app/src/beans.beans.dart';

@ComponentScan()
final BeanModule appBeans = $appBeans;
abstract interface class Greeter {
  String greet(String name);
}

@Service()
class EnglishGreeter implements Greeter {
  EnglishGreeter(@Value(r'${app.title}') this.title);

  final String title;

  @override
  String greet(String name) => 'Hello $name from $title';
}
dart run build_runner build   # or watch
Future<void> main(List<String> args) async {
  final environment = StandardEnvironment()
    ..propertySources.addFirst(SimpleCommandLinePropertySource(args));
  await ConfigDataEnvironment(environment).processAndApply();

  final context = ApplicationContext.run([appBeans], environment: environment);
  print(context.getBean<Greeter>().greet('Ada'));
  context.close();
}

Guide #

Declaring beans #

Annotation Meaning
@Component([name]), @Service, @Repository, @Controller, @Named A bean. Default name: lowerCamelCase class name (URLService stays). Subclass Component for your own stereotypes.
@Configuration([name]) A bean whose @Bean methods declare more beans. @Lazy / @Profile / conditions on it apply to its @Bean methods.
@Bean(name:, aliases:, initMethod:, destroyMethod:, autowireCandidate:) A bean produced by a method; type = declared return type; parameters are injected. Destroy method is inferred (close, shutdown, dispose) unless destroyMethod: ''.
@Scope('prototype') / ScopeType.* / custom name Scope; custom scopes via registerScope(name, CustomScope) (e.g. SimpleMapScope).
@Lazy() Singletons are eager by default; lazy ones are created on first use.
@Primary(), @Fallback(), @Qualifier('x') Choosing between candidates.
@DependsOn([...]), @Order(n), @Description('...') Creation order, collection order, documentation.

A bean is registered under its own type and all its public supertypes. Generic supertypes are matched by the full type (Store<User>). A FactoryBean<T> exposes its product by name and type; &name returns the factory.

Injection #

@Service()
class OrderService {
  OrderService(
    this.repository,                         // single bean
    @Qualifier('fast') this.cache,          // by name / qualifier
    this.listeners,                          // List<Listener>: all, ordered
    {this.audit,                             // nullable = optional
    required this.reports,                   // ObjectProvider<Report>: lazy
    @Value(r'${orders.limit:100}') required this.limit});

  @Autowired()
  late final Clock clock;                    // field

  @Autowired()
  set metrics(Metrics value) {}              // setter

  @Autowired()
  void init(Environment environment) {}      // method

  ...
}

@Lazy() on an injection point injects a generated proxy that finds the bean on first use (Spring's lazy-resolution proxy). It breaks cycles and lets a bean call itself through its AOP proxy:

@Service()
class BookService implements Books {
  BookService(@Lazy() this._self);   // also on fields and setters
  final Books _self;

  @Timed()
  @override
  Book find(String isbn) => ...;

  List<Book> findAll(List<String> isbns) =>
      [for (final isbn in isbns) _self.find(isbn)];   // advised; this.find() is not
}

The bean must exist and be unique (checked at refresh). The type must be an interface or a class without private members, not generic, and the point must not be nullable; otherwise the build stops and suggests ObjectProvider<T>.

Primary constructors and other new syntax

Dart 3.13+ syntax works with the generator: primary constructors (also on @Configuration and @Introspected classes, with annotations on their parameters), class const, in-body new(...) / new name(...) / factory name(...) constructors and private named parameters (injected under their public name).

@Component()
class Settings(@Value(r'${pool.size:1}') final int capacity);

@Service()
class Inventory {
  final Settings settings;
  final Clock _clock;
  new(this.settings, {required this._clock}); // called as clock: ...
}

@Service()
class Report {
  final Inventory inventory;
  new _(this.inventory);

  @Autowired()
  factory of(Inventory inventory) => Report._(inventory);
}

@Introspected()
class StockForm(@NotBlank() var String sku, {@Min(1) var int quantity = 1});

Selection when several beans match: the only candidate, else the single @Primary, else the only non-@Fallback, else the one named like the parameter. A bean is never injected into itself (Spring's self-reference rule), so a @Primary composite or decorator of Greeter receives the other greeters. A constructor or @Bean parameter with a Dart default value keeps the default when no bean (or property) is found, and a required List/Set/Map there is empty when nothing matches (Spring's single-constructor fallback; field and setter injection still require a bean). List<T> / Set<T> / Map<String, T> receive every candidate (also from parent contexts) sorted by PriorityOrdered → Ordered → @Order. BeanFactory, ApplicationContext, ApplicationEventPublisher, ResourceLoader and Environment can be injected.

@Value resolves ${key:default} placeholders, evaluates #{...} SpEL and converts the result to the target type (int, Duration, List<String>, enums with addEnum, ...). A nullable target gets null when a placeholder has no value.

refresh() validates the whole graph before creating the application's beans (missing and ambiguous dependencies, unresolvable placeholders, @DependsOn names, cycles), so configuration errors fail at startup. Only post-processors and infrastructure beans (messageSource, ...) are created before that check.

Lifecycle #

Creation: constructor → fields/setters/methods → BeanNameAware / BeanFactoryAware → context aware interfaces (EnvironmentAware, ApplicationContextAware, ...) → postProcessBeforeInitialization → @PostConstruct → InitializingBean.afterPropertiesSet → init method → postProcessAfterInitialization. Destruction (on close(), dependents first): DestructionAwareBeanPostProcessor → @PreDestroy → DisposableBean.destroy → destroy method.

SmartInitializingSingleton runs after all eager singletons; Lifecycle / SmartLifecycle beans are started by phase (auto-startup at refresh) and stopped in reverse on stop() / close(). Like Spring's doClose, close() never throws because of a failing ContextClosedEvent listener or a bean that fails to stop: the error is logged and shutdown continues (a close() from inside a listener is ignored).

Problems the framework recovers from are logged through SpringLog (warnings are printed by default): set SpringLog.level and SpringLog.handler to route them, e.g. to package:logging.

Environment and configuration #

  • StandardEnvironment: property sources (command line, OS environment, config files, @PropertySource), ${...} placeholders with defaults, nesting and escapes, typed getPropertyAs<T>, required properties.
  • Profiles: spring.profiles.active / .default, expressions dev & (eu | us) & !prod.
  • ConfigDataEnvironment (Spring Boot config data): application.properties and application-{profile}.properties from classpath:/ and classpath:/config/ (= lib/resources/), overridden by file:./ and file:./config/ (or spring.config.location / additional-location / name), multi-doc files with #--- and spring.config.activate.on-profile, spring.config.import, spring.profiles.include / group.*, optional: locations. Works with async-only resources (Flutter assets) via processAndApply().
  • @PropertySource([...], factory: ...) on a component adds files; the last declared wins. It is loaded only when the declaring class's conditions (@Profile, @Conditional, ...) match; classes pulled in with @Import inherit the importer's conditions.
  • YAML: implement PropertySourceLoader (yml, yaml) for config data and PropertySourceFactory for @PropertySource — the core needs no change.

There is no .env support by design: configuration lives in property files.

Conditions #

@Profile(['dev']), @Conditional([MyCondition]), or any const annotation class that implements Condition (attribute-style conditions such as @ConditionalOnProperty('feature.x')). Conditions see the environment and the definitions registered before them.

Modules #

@ComponentScan generates a BeanModule per package (directory). Combine modules in the application; @Import([OtherConfig]) pulls in configuration classes outside the scan. Contexts can have a parent; beans of the parent are visible to the child.

Events #

@Component()
class Shipping {
  @EventListener()                       // payloads or ApplicationEvents
  OrderShipped onPlaced(OrderPlaced order) => OrderShipped(this, order.id);

  @EventListener(condition: isBigOrder)  // top-level / static predicate
  @Order(1)
  void onBig(OrderPlaced order) {}

  @EventListener(classes: [ContextRefreshedEvent, ContextClosedEvent])
  void onContext() {}
}

Non-null results are published again. ApplicationListener<E> beans work too; context.addListener<E>(...) returns a removable registration. Events published during refresh are delivered once listeners are ready; events propagate to the parent context.

Messages, resources, conversion #

  • MessageSource: StaticMessageSource, PropertiesMessageSource (messages_ru_RU.properties → messages_ru → base), locale fallback, MessageFormat placeholders, quotes, number, choice. Define a bean named messageSource; the context delegates to it.

  • Resources: lib/resources/ is the classpath — the generator embeds its files, so classpath:... works in CLI, AOT, Flutter and web without asset declarations; @Value('classpath:templates/mail.txt') Resource template. ResourceLoader with classpath:, file: and custom protocols (ProtocolResolver), PathMatchingResourcePatternResolver with AntPathMatcher.

    Why lib/resources/ and not bin/ or the project root: lib/ is the only part of a Dart package that other packages can use, so a starter keeps its defaults and messages in its own lib/resources/ and they join the application's classpath exactly like the application's files (its generated module embeds them). Files elsewhere would stay invisible for dependencies and would be missing in AOT executables, Flutter apps and the web, which have no project folder at runtime. An app with only bin/main.dart still keeps its resources in lib/resources/; lib/ may contain nothing else:

    my_app/
      bin/main.dart              # @SpringBootApplication
      lib/resources/
        application.properties   # classpath:application.properties
    

    Tests see test/resources/ first (spring_boot_starter_test), like src/test/resources before src/main/resources in Java.

  • DefaultConversionService with Spring Boot duration syntax (10s, PT5M), collections and enums.

SpEL #

final parser = SpelExpressionParser();
parser.parseExpression("name.toUpperCase()").getValue(context, user);
parser.parseExpression('Total: #{price * 2}', ParserContext.template).getValue(context);

Literals, inline lists/maps, properties, indexing, safe navigation (?., short-circuiting the chain), methods, variables and functions, T(Type) statics, constructors, @bean references, selection/projection, assignment and ++/--, all operators (div, between, matches, Elvis, ternary). Dart has no reflection: user classes are reached through @Introspected metadata, FunctionPropertyAccessor / FunctionMethodResolver or maps. SimpleEvaluationContext restricts expressions for data binding.

Validation and data binding #

@Introspected()
@PasswordsMatch()                   // class-level constraint
class SignupForm {
  @NotBlank() @Size(max: 20) String name = '';
  @NotNull() @Email() String? email;
  @Min(18) int age = 0;
  @Valid() Address address = Address();
}

final binder = DataBinder(form, objectName: 'signup')
  ..validators.add(BeanValidator(introspector: context.getBean<BeanIntrospector>()))
  ..bind({'name': 'Ada', 'age': '36'})
  ..validate();
binder.bindingResult.fieldError('age');   // typeMismatch / Min ... with Spring codes

Constraints: NotNull, MustBeNull, NotEmpty, NotBlank, Size, Min, Max, Positive(OrZero), Negative(OrZero), Matches (@Pattern), Email, Digits, AssertTrue/False, PastDate, FutureDate and ...OrPresentDate, groups, @Valid cascade, custom Constraint subclasses. Error codes and arguments follow Spring (NotBlank.signup.name, NotBlank.name, NotBlank.String, NotBlank), so messages come from a MessageSource.

AOP #

AOP (aspect-oriented programming) runs your code before, after or around methods of other beans without editing them. Use it for things that cut across many classes: logging, timing, auditing, permission checks, retries, caching.

Term What it is In code
Aspect A bean holding the extra code a class with @Aspect() and @Component()
Advice One piece of extra code and when it runs a method with @Before, @After, @AfterReturning, @AfterThrowing or @Around
Pointcut Which methods the advice applies to the string in the annotation: '@annotation(Timed)', 'execution(* OrderService.*(..))'
Join point One intercepted call the JoinPoint / ProceedingJoinPoint parameter
Proxy The generated wrapper that calls the advice written by the generator, injected instead of the bean

A first aspect

Time every method marked @Timed():

// An annotation is a plain class with a const constructor.
class Timed {
  const Timed();
}

@Aspect()
@Component()
class TimingAspect {
  @Around('@annotation(Timed)')
  Object? time(ProceedingJoinPoint call) {
    final watch = Stopwatch()..start();
    try {
      return call.proceed(); // runs the real method
    } finally {
      print('${call.signature.name}: ${watch.elapsedMilliseconds} ms');
    }
  }
}

@Service()
class Search {
  @Timed()
  List<String> find(String text) => ['dart', 'spring'];
}

// context.getBean<Search>().find('d'); // prints "find: 0 ms"

How it works: the generator (spring_core_dart_generator, required) writes a proxy class for each bean. When the context starts, every bean that some pointcut matches is replaced by its proxy, and the proxy is what other beans and getBean receive. A call to search.find(...) goes through the proxy, which runs the advice. Beans that no pointcut matches are not wrapped and cost nothing.

Rules to remember:

  • The aspect must be a bean: @Aspect() plus @Component() (or a @Bean method). Aspects can inject other beans as usual; they are never proxied themselves.
  • Only calls from outside go through the proxy. A call through this inside the bean (find calling count) skips the advice, exactly as in Spring. Call through another bean, or inject the bean into itself with @Lazy() (see Injection) or ObjectProvider<T>.
  • The proxy implements the bean class or its interfaces, so the bean needs public members or an interface with the advised methods (the error message says which). See "Proxies" below.

Kinds of advice

Annotation Runs Parameters (all optional unless noted) Can change the result
@Before before the method JoinPoint no; throwing stops the call
@After after the method, whether it returned or threw (like finally) JoinPoint no
@AfterReturning after it returned normally JoinPoint, then the result no
@AfterThrowing after it threw; the error is rethrown afterwards JoinPoint, then the error no
@Around instead of the method; you decide if and how to call it ProceedingJoinPoint (required) yes
@Aspect()
@Component()
class AuditAspect {
  // A named pointcut: the method body is empty, its name is the pointcut.
  @Pointcut('within(OrderService)')
  void orders() {}

  @Before('orders()')
  void before(JoinPoint call) => print('calling ${call.toShortString()} '
      'with ${call.args}');

  // Runs only when the result is a double: the parameter type filters.
  @AfterReturning('orders()')
  void returned(JoinPoint call, double total) => print('total: $total');

  // Runs only for StateError; other errors pass by.
  @AfterThrowing('orders()')
  void failed(StateError error) => print('failed: ${error.message}');

  @After('orders()')
  void after() => print('done');

  // Change arguments, the result, or skip the call entirely.
  @Around('execution(* OrderService.discount(..))')
  Object? clamp(ProceedingJoinPoint call) {
    final [percent] = call.args;
    return call.proceed([(percent! as int).clamp(0, 50)]);
  }
}

Parameters are positional and matched by type: JoinPoint / ProceedingJoinPoint by exact type, and in @AfterReturning / @AfterThrowing the first other parameter gets the result or the error. Anything else is a build error.

Async methods: for a method returning a Future, @After, @AfterReturning and @AfterThrowing run when the future completes (with its value or error), not when the method returns. In @Around, proceed() returns the future; make the advice async and await it to work with the value:

@Around('execution(Future *(..))')
Future<Object?> timeAsync(ProceedingJoinPoint call) async {
  final watch = Stopwatch()..start();
  try {
    return await (call.proceed()! as Future<Object?>);
  } finally {
    print('${call.signature.name}: ${watch.elapsedMilliseconds} ms');
  }
}

What a JoinPoint gives you:

Member Example value
signature.name find
signature.declaringType Search
signature.targetType the bean class, e.g. DefaultSearch
signature.returnType, signature.parameterTypes List, [String]
signature.kind MemberKind.method, getter or setter
signature.annotations the method's annotation objects, e.g. [Timed()]
args the arguments, ['d']
target, proxy the bean, the proxy
toShortString() execution(Search.find(..))
proceed([args]) (ProceedingJoinPoint only) calls the next advice or the method; args replaces the arguments

Pointcut syntax

A pointcut is one or more designators combined with operators:

designator(pattern)  [&& | || | !  designator(pattern) ...]
Designator Matches Example
execution(ret Type.name(params)) methods by return type, declaring type, name and parameters execution(* OrderService.find*(..))
within(TypePattern) every member of beans whose class matches within(*Repository)
target(Type) every member of beans of that type or a subtype (interfaces too) target(PaymentGateway)
bean(namePattern) every member of beans with that name bean(order*)
@annotation(Annotation) methods carrying the annotation (also when it is on the interface method they implement) @annotation(Timed)
@within(Annotation) every member of beans whose class carries the annotation @within(Service)
name() a named pointcut: a @Pointcut method of any aspect orders(), AuditAspect.orders()

Patterns:

  • * in a name matches any characters: find*, *Service, *.
  • In execution parameters, .. matches any number of parameters (including none) and * exactly one: (..), (), (String, ..), (*, int).
  • The return type comes first; * means any: execution(* *(..)), execution(void *(..)), execution(Future *(..)). The type before the method name is optional: execution(* find*(..)) matches find* methods of every bean.
  • Generics and ? are ignored when matching: Future<int> matches Future, int? matches int.
  • Type names are simple names. Dart has no packages, so com.x.Service means Service, and a package wildcard like within(com.x..*) is an error instead of silently matching everything.

Operators: && / and, || / or, ! / not, and parentheses.

Examples:

Pointcut Advises
execution(* *(..)) every method of every bean (methods only, not getters/setters)
within(OrderService) everything of the OrderService bean, getters and setters included
execution(* OrderService.*(..)) the methods of OrderService
execution(* find*(..)) methods whose name starts with find, in any bean
execution(Future *(..)) async methods
execution(void *(String, ..)) void methods whose first parameter is a String
target(PaymentGateway) every bean implementing PaymentGateway
bean(order*) beans named order…
@annotation(Timed) methods marked @Timed()
@within(Service) every member of @Service classes
within(*Service) && !execution(* get*(..)) services, except get… methods
orders() || bean(payments) a named pointcut or the payments bean

Named pointcuts are shared by all aspects: refer to them as orders() or, to be explicit, AuditAspect.orders().

Not supported yet: argument binding (args(...), @annotation(timed) with a parameter name). Read annotations from joinPoint.signature.annotations instead. An invalid pointcut fails when the context starts, with the reason; the Zed extension (dart_spring) underlines it while you type.

Order of advice

  • Several aspects: @Order on the aspect class decides; the lower value is the outer one (its @Before runs first, its @After last). Aspects without @Order come last.
  • Inside one aspect: around → before → after → afterReturning → afterThrowing, as in Spring.
@Aspect() @Component() @Order(1) class SecurityAspect { ... } // outer
@Aspect() @Component() @Order(2) class TimingAspect { ... }   // inner

Advice without annotations

A PointcutAdvisor bean is applied like an aspect. Write the pointcut as an ExpressionPointcut (same syntax) or as your own AopPointcut class:

class UpperCase implements MethodInterceptor {
  @override
  Object? invoke(MethodInvocation invocation) =>
      (invocation.proceed()! as String).toUpperCase();
}

@Configuration()
class AopConfig {
  @Bean()
  PointcutAdvisor upperCase() =>
      DefaultPointcutAdvisor(ExpressionPointcut('bean(slogans)'), UpperCase());
}

Proxies

Proxies are generated classes that implement the bean class (when it has no private members and is not final, base or sealed) and its interfaces, so the bean can be injected by either. If advice matches a bean that cannot be proxied, the context fails at startup and says why; add an interface with the advised methods and inject the bean by it. AopUtils.getTarget(bean) unwraps a proxy. The generator option aop_proxies (all by default, interfaces, none) controls which proxies are generated (see spring_core_dart_generator). Annotations are matched by names the generator writes, so @annotation(Timed) also works in minified and obfuscated release builds.

Logging #

SpringLog is the logging facade (Spring's commons-logging / SLF4J): the framework, starters and applications log through it; a backend such as spring_boot_starter_logging decides where records go. Without one, warnings and errors are printed.

const log = SpringLog('neon.checkout');

log.info('order placed', fields: {'order': 42});
log.debug(() => 'basket: ${basket.dump()}');   // built only if enabled
log.error('payment failed', error: e, stackTrace: s);

SpringLog.setLevel('neon.checkout', SpringLogLevel.debug); // and below
SpringLog.withFields({'request': id}, () => handle(request)); // like MDC

With Spring Boot, levels come from logging.level.* properties.

Other extension points #

BeanFactoryPostProcessor / BeanDefinitionRegistryPostProcessor, BeanPostProcessor, custom scopes, hand-written BeanDefinitions and BeanModules, registerSingleton, @Configurable + autowireBean for objects created elsewhere (e.g. Flutter State).

Your own method annotations: mark the annotation class with @BeanMethodAnnotation() and the generator lists every bean method that carries it in BeanDefinition.annotatedMethods, with its annotations and a closure that calls it — what Spring does with reflection (MethodIntrospector). This is how @Scheduled finds its methods:

@BeanMethodAnnotation()
class Poll {
  const Poll(this.every);
  final Duration every;
}

@Component()
class Inbox {
  @Poll(Duration(seconds: 30))
  void check() { ... }
}

// In a BeanPostProcessor (with BeanFactoryAware) or after refresh:
for (final method in beanFactory.getBeanDefinition(name).annotatedMethods) {
  final poll = method.annotations.whereType<Poll>().firstOrNull;
  if (poll != null) {
    Timer.periodic(poll.every, (_) => method.invoke(bean, const []));
  }
}

Differences from Spring #

Spring Here Why
Reflection, runtime classpath scanning build_runner code generation, @ComponentScan per directory No reflection in Flutter/AOT
PropertySource class AbstractPropertySource @PropertySource is the annotation
Scope SPI, Pointcut interface CustomScope, AopPointcut @Scope, @Pointcut are the annotations
@Null, @Pattern, @Past, @Future MustBeNull, Matches, PastDate, FutureDate (codes unchanged) Clashes with Dart core types
Validator.supports(Class) supports(Object target) Dart cannot test subtypes on a Type
CGLIB @Configuration proxies Call other @Beans through parameters No subclass proxies
CGLIB / JDK proxies Generated implements proxies Final or private-member classes are proxied through interfaces only
Circular references (opt-in) Always an error Same as Spring Boot's default
Asynchronous initialization Synchronous refresh(); load config first with processAndApply() Predictable startup
Package-qualified AOP type patterns, argument binding Simple type names; qualified wildcards and binding are rejected No packages in Dart; binding not implemented yet
Optional @Value with ${x:#{null}} A nullable target gets null when a placeholder is missing Dart null safety

Roadmap #

  • Flutter library: bean builders, event listener widgets, @Configurable State injection, asset resources.
  • Spring Web and a Boot starter with auto-configuration (@ConditionalOn...).
  • Rust bridge over rinf: Dart beans in Rust and Rust beans in Dart.

License #

Apache License 2.0. Spring is a trademark of Broadcom Inc.; this project is an independent port and is not affiliated with or endorsed by the Spring team.

0
likes
0
points
829
downloads

Publisher

verified publisherfarum-azula.org

Weekly Downloads

Spring Core for Dart and Flutter: annotation-based dependency injection, application.properties configuration, profiles, events, SpEL, validation and AOP.

Repository (GitHub)
View/report issues

Topics

#dependency-injection #inversion-of-control #configuration #aop #spring

License

unknown (license)

More

Packages that depend on spring_core_dart