CodeBlanks Logo Icon CodeBlanks

Architecture

Combine layers with clean architecture.

Architecture

Now we add architecture layers: a MathRepository (data access abstraction) and a ServiceContainer (DI container). This combines clean architecture + repository + DI patterns.

MathRepository takes a MathService and offers a sum method. ServiceContainer registers services with register, resolves with resolve<T>.

These layers let main depend on interfaces, not concrete classes. In tests a mock service is injected; in production the native service. The same repository works in both.

Repository + DI together

MathRepository depends on the MathService interface. The service resolved from ServiceContainer is given to the repository. This is a concrete application of dependency inversion.

Flow: container.register('math', NativeMathService(...)) → container.resolve<MathService>('math') → MathRepository(service) → repo.sum(4, 6).

This chain knows the concrete class in only one place (register). All other code works with the interface. In tests MockMathService is registered; other code stays unchanged.

Practice Add MathRepository and ServiceContainer

The BindingGenerator is already defined below. Now define a class MathRepository with final MathService service; MathRepository(this.service); int sum(int a, int b) => service.add(a, b);. Define a class ServiceContainer with final Map<String, dynamic> _services = {}; void register(String key, dynamic service) => _services[key] = service; T resolve<T>(String key) => _services[key] as T;. In main create final container = ServiceContainer();; call container.register('math', NativeMathService('native_add'));; add final repo = MathRepository(container.resolve<NativeMathService>('math')); and print('repo: ${repo.sum(4, 6)}');.

Starter code

main.dart
import 'dart:ffi';

abstract class MathService {
  int add(int a, int b);
}

typedef NativeAdd = Int32 Function(Int32 a, Int32 b);
typedef DartAdd = int Function(int a, int b);

class NativeMathService implements MathService {
  final String bindingName;
  NativeMathService(this.bindingName);
  @override
  int add(int a, int b) => a + b;
  String describe() => 'native: $bindingName';
}

class NativeBinding {
  final String name;
  const NativeBinding(this.name);
}

class BindingGenerator {
  String generate(String className) => '// generated binding for $className';
}

// define a MathRepository class (service field, sum method)
// define a ServiceContainer class (register, resolve methods)

void main() {
  print('service ready');
  final svc = NativeMathService('native_add');
  print(svc.describe());
  print('result: ${svc.add(2, 3)}');
  final gen = BindingGenerator();
  print(gen.generate('NativeMathService'));
  // create a container; register a service; print sum via repo
}

Sample solution

main.dart
import 'dart:ffi';

abstract class MathService {
  int add(int a, int b);
}

typedef NativeAdd = Int32 Function(Int32 a, Int32 b);
typedef DartAdd = int Function(int a, int b);

class NativeMathService implements MathService {
  final String bindingName;
  NativeMathService(this.bindingName);
  @override
  int add(int a, int b) => a + b;
  String describe() => 'native: $bindingName';
}

class NativeBinding {
  final String name;
  const NativeBinding(this.name);
}

class BindingGenerator {
  String generate(String className) => '// generated binding for $className';
}

class MathRepository {
  final MathService service;
  MathRepository(this.service);
  int sum(int a, int b) => service.add(a, b);
}

class ServiceContainer {
  final Map<String, dynamic> _services = {};
  void register(String key, dynamic service) => _services[key] = service;
  T resolve<T>(String key) => _services[key] as T;
}

void main() {
  print('service ready');
  final svc = NativeMathService('native_add');
  print(svc.describe());
  print('result: ${svc.add(2, 3)}');
  final gen = BindingGenerator();
  print(gen.generate('NativeMathService'));
  final container = ServiceContainer();
  container.register('math', NativeMathService('native_add'));
  final repo = MathRepository(container.resolve<NativeMathService>('math'));
  print('repo: ${repo.sum(4, 6)}');
}

Expected output

service ready
native: native_add
result: 5
// generated binding for NativeMathService
repo: 10

Questions and answers

Review the lesson questions together with their answers. To test yourself, solve the lesson interactively.

1 Match the architecture parts

Term Meaning
MathRepository Data access
ServiceContainer DI container
register Registration method
resolve<T> Resolve method

2 What does ServiceContainer do?

  • Opens files
  • Listens on network
  • Registers and resolves services Correct answer
  • Runs tests

Explanation: ServiceContainer registers with register, resolves with resolve.

3 The register call

Question
container.____('math', svc);
Answer
container.register('math', svc);

Explanation: register adds a service to the container.

All lessons in Expert