The container and beans

The container is the part of spring_core_dart that creates the objects of your application, passes each one the other objects it needs, and shuts them down at the end.

An object the container manages is called a bean. You mark a class with an annotation such as @Service(). A build step (spring_core_dart_generator) reads the annotations and writes plain Dart code that creates the beans. At run time an ApplicationContext loads that code, creates every bean once and connects them.

Warning

Checked at startup: a missing or ambiguous bean, a cycle or an unknown placeholder stops the start with a message naming the bean and the parameter.

Why use a container

In a program of some size, main ends up building every service by hand, in the right order, and passing each one its dependencies. Every new constructor parameter means editing main and every test that builds the class. Choosing a fake implementation in development means an if somewhere.

With the container:

You do not need it for a script with two classes. It pays off once several services depend on each other, or when tests and environments need different implementations.

Dart in Flutter and AOT executables has no runtime reflection. That is why the annotations are read at build time: the generator writes the wiring code, and the container runs it. There is no scanning or reflection at run time.

A complete example

Add the packages:

dependencies:
  spring_core_dart: ^0.7.1

dev_dependencies:
  build_runner: ^2.17.0
  spring_core_dart_generator: ^0.7.1

Declare where the beans are. Every class in the directory of this file and below is scanned:

// lib/src/beans.dart
import 'package:spring_core_dart/spring_core_dart.dart';
// Written by the generator; it exists after the first build.
import 'package:my_app/spring_gen/src/beans.spring.dart';

@ComponentScan()
final BeanModule appBeans = $appBeans;

Write the beans. Each one asks for its dependencies in the constructor:

// lib/src/orders.dart
import 'package:spring_core_dart/spring_core_dart.dart';

class Order {
  Order(this.id, this.total);
  final int id;
  final double total;
}

// Stores orders. A real application would talk to a database here.
@Repository()
class OrderRepository {
  final _orders = <int, Order>{};

  void save(Order order) => _orders[order.id] = order;
  Order? findById(int id) => _orders[id];
}

// Business logic. The container passes the OrderRepository bean and
// the `orders.limit` setting (100 when it is not configured).
@Service()
class OrderService {
  OrderService(this.repository, @Value(r'${orders.limit:100}') this.limit);

  final OrderRepository repository;
  final int limit;

  Order place(int id, double total) {
    if (total > limit) {
      throw ArgumentError('Order $id is over the limit of $limit');
    }
    final order = Order(id, total);
    repository.save(order);
    return order;
  }
}

Generate the wiring code, again after every change to the annotations:

dart run build_runner build   # or: dart run build_runner watch

Start the container, use a bean, close it:

// bin/main.dart
import 'package:my_app/src/beans.dart';
import 'package:my_app/src/orders.dart';
import 'package:spring_core_dart/spring_core_dart.dart';

const log = SpringLog('my_app');

void main() {
  // Creates every bean and injects the dependencies.
  final context = ApplicationContext.run([appBeans]);

  final orders = context.getBean<OrderService>();
  final order = orders.place(1, 42);
  log.info('placed order ${order.id}');

  // Runs the @PreDestroy methods and releases the beans.
  context.close();
}

main never calls OrderRepository() or OrderService(...). Adding a parameter to OrderService means changing only OrderService.

With spring_boot_dart, the configuration files are read for you and main becomes one line. This section describes the container that both use.

Pages in this section

Declaring beans:

Shaping a bean:

Injection:

Lifecycle:

The container itself:

See also