Dart
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.
Exercise 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)}');.
Show example solution
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)}');
}
Test yourself
-
Match the architecture parts
- MathRepository
- Data access
- ServiceContainer
- DI container
- register
- Registration method
- resolve<T>
- Resolve method
-
What does ServiceContainer do?
- Opens files
- Listens on network
- Registers and resolves services
- Runs tests
Show answer
Registers and resolves services — ServiceContainer registers with register, resolves with resolve.
-
The register call
container.____('math', svc);
Show answer
container.register('math', svc);
Next lesson: Putting it together