@ComponentScan

@ComponentScan() makes the generator write a module with every bean declared in the directory of the annotated file and its subdirectories.

Why

Dart in Flutter and AOT executables cannot search the program for annotated classes at run time. So the search happens at build time: build_runner runs spring_core_dart_generator, which reads the files under the directory and writes a BeanModule. You pass that module to the context.

Put @ComponentScan() on a top-level variable. Initialize it with the generated module, which has the same name prefixed with $. The generated file sits under lib/spring_gen/, mirroring the path of your file.

A Spring Boot application uses @SpringBootApplication() instead, which scans too.

Nested scans: a subdirectory with its own @ComponentScan belongs to that scan, and its filters apply there. The enclosing scan imports its module instead of scanning its files again, so every bean is registered once. One directory has one scan root.

Filters

Filters change what counts as a bean. They are evaluated at build time.

A Filter matches in one of three ways (FilterType):

type Uses Matches a class that
FilterType.annotation (default) classes carries one of the annotations (or a subclass of one).
FilterType.assignableType classes is, extends, implements or mixes in one of the types.
FilterType.regex pattern has a name matching one of the regular expressions.

Example

// lib/src/beans.dart: everything under lib/src/ is scanned.
import 'package:spring_core_dart/spring_core_dart.dart';
import 'package:my_app/spring_gen/src/beans.spring.dart';

@ComponentScan()
final BeanModule appBeans = $appBeans;
// lib/src/plugins/beans.dart: a scan with filters.
import 'package:spring_core_dart/spring_core_dart.dart';
import 'package:my_app/spring_gen/src/plugins/beans.spring.dart';

// Every Plugin class is a bean without @Component, classes whose name
// ends in Stub never are, and every bean is created on first use.
@ComponentScan(
  includeFilters: [
    Filter(type: FilterType.assignableType, classes: [Plugin]),
  ],
  excludeFilters: [
    Filter(type: FilterType.regex, pattern: [r'.*Stub$']),
  ],
  lazyInit: true,
)
final BeanModule pluginBeans = $pluginBeans;

abstract interface class Plugin {
  String get id;
}
// bin/main.dart
void main() {
  // appBeans already imports pluginBeans (nested scan); passing both
  // is harmless.
  final context = ApplicationContext.run([appBeans]);
  context.close();
}

Run the generator after changing annotations:

dart run build_runner build

Parameters

ComponentScan:

Name Type Default Meaning
includeFilters List<Filter> [] Classes matching one of these are beans even without @Component.
excludeFilters List<Filter> [] Classes matching one of these are never beans.
useDefaultFilters bool true Whether @Component and its subclasses mark a bean.
lazyInit bool false Whether the scanned beans are created on first use.

Filter:

Name Type Default Meaning
type FilterType FilterType.annotation How the filter matches.
classes List<Type> [] Annotation classes, or types the class must be assignable to.
pattern List<String> [] Regular expressions matched against the class name.

In Spring (Java)

@Configuration
@ComponentScan(
    basePackages = "com.example.plugins",
    includeFilters = @ComponentScan.Filter(
        type = FilterType.ASSIGNABLE_TYPE, classes = Plugin.class),
    excludeFilters = @ComponentScan.Filter(
        type = FilterType.REGEX, pattern = ".*Stub"),
    lazyInit = true)
class PluginConfig {}

var context = new AnnotationConfigApplicationContext(PluginConfig.class);

Differences that matter:

See also