Skip to content

Commit 25be73b

Browse files
committed
feat: implement Injectable interface for service and datasource classes
1 parent c170342 commit 25be73b

File tree

7 files changed

+128
-9
lines changed

7 files changed

+128
-9
lines changed

lib/src/domain/datasources/datasource.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import 'package:grumpy/grumpy.dart';
99
/// A datasource is responsible for providing data from a specific source,
1010
/// such as a database, API, or local storage.
1111
@BaseClass(allowedLayers: {.domain, .infra})
12-
abstract class Datasource with LogMixin, Disposable, TelemetryMixin {
12+
abstract class Datasource
13+
with LogMixin, Disposable, TelemetryMixin
14+
implements Injectable {
1315
/// A datasource is responsible for providing data from a specific source,
1416
/// such as a database, API, or local storage.
1517
const Datasource();
@@ -28,4 +30,7 @@ abstract class Datasource with LogMixin, Disposable, TelemetryMixin {
2830

2931
/// Retrieves an instance of the specified [Datasource] type from the service locator.
3032
static D get<D extends Datasource>() => GetIt.instance<D>();
33+
34+
@override
35+
bool get singelton => false;
3136
}

lib/src/domain/domain.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,23 @@ export 'models/models.dart';
22
export 'datasources/datasources.dart';
33
export 'services/services.dart';
44
export 'errors/errors.dart';
5+
6+
import 'package:get_it/get_it.dart' hide Disposable;
7+
8+
/// Base contract for DI-managed types.
9+
///
10+
/// Concrete implementations can control their registration behavior in
11+
/// [Module.bindServices] and [Module.bindDatasources] through [singelton]:
12+
/// if `true`, modules register them as lazy singletons; if `false`, modules
13+
/// register them as factories.
14+
abstract class Injectable {
15+
/// Creates an injectable DI contract.
16+
const Injectable();
17+
18+
/// Whether this type should be resolved as a singleton in module DI binding.
19+
///
20+
/// Used by module injectable binding:
21+
/// - `true` => [GetIt.registerLazySingleton]
22+
/// - `false` => [GetIt.registerFactory]
23+
bool get singelton;
24+
}

lib/src/domain/services/routing_service.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ abstract class RoutingService<T, Config extends Object> extends Service {
7474
factory RoutingService() {
7575
return Service.get<RoutingService<T, Config>>();
7676
}
77+
78+
@override
79+
bool get singelton => true;
7780
}
7881

7982
/// An event representing a change in the view rendered by the [RoutingService].

lib/src/domain/services/service.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import 'package:grumpy/grumpy.dart';
99
/// A service is responsible for IO operations, such as making network requests
1010
/// or reading/writing files.
1111
@BaseClass(allowedLayers: {.domain, .infra})
12-
abstract class Service with LogMixin, Disposable, TelemetryMixin {
12+
abstract class Service
13+
with LogMixin, Disposable, TelemetryMixin
14+
implements Injectable {
1315
/// A service is responsible for IO operations, such as making network requests
1416
/// or reading/writing files.
1517
const Service();
@@ -27,4 +29,7 @@ abstract class Service with LogMixin, Disposable, TelemetryMixin {
2729

2830
/// Retrieves an instance of the specified [Service] type from the service locator.
2931
static S get<S extends Service>() => GetIt.instance<S>();
32+
33+
@override
34+
bool get singelton => false;
3035
}

lib/src/module.dart

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ abstract class Module<RouteType, Config extends Object>
7171
log('${module.runtimeType} mounted successfully.');
7272
}
7373

74+
void _bindInjectable<T extends Injectable>(Builder<T, Config> builder) {
75+
final instance = builder(_di.get<Config>(), _di.get);
76+
77+
if (instance.singelton) {
78+
_di.registerLazySingleton<T>(() => builder(_di.get<Config>(), _di.get));
79+
} else {
80+
_di.registerFactory<T>(() => builder(_di.get<Config>(), _di.get));
81+
}
82+
}
83+
7484
@mustCallSuper
7585
@override
7686
FutureOr<void> activate() async {
@@ -107,14 +117,12 @@ abstract class Module<RouteType, Config extends Object>
107117
_di.registerSingleton<T>(builder(_di.get<Config>(), _di.get));
108118
});
109119

110-
bindServices(<T extends Service>(Builder<Service, Config> builder) {
111-
_di.registerFactory<T>(() => builder(_di.get<Config>(), _di.get) as T);
120+
bindServices(<T extends Service>(Builder<T, Config> builder) {
121+
_bindInjectable<T>(builder);
112122
});
113123

114-
bindDatasources(<T extends Datasource>(
115-
Builder<Datasource, Config> builder,
116-
) {
117-
_di.registerFactory<T>(() => builder(_di.get<Config>(), _di.get) as T);
124+
bindDatasources(<T extends Datasource>(Builder<T, Config> builder) {
125+
_bindInjectable<T>(builder);
118126
});
119127

120128
bindRepos(<T extends Repo>(Builder<Repo, Config> builder) {

lib/src/presentation/repositories/repo.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import 'package:rxdart/rxdart.dart';
1212
///
1313
/// See [RepoState] for more details on the possible states.
1414
abstract class Repo<T>
15-
with LogMixin, LifecycleMixin, LifecycleHooksMixin, Disposable {
15+
with LogMixin, LifecycleMixin, LifecycleHooksMixin, Disposable
16+
implements Injectable {
1617
final _stream = BehaviorSubject.seeded(RepoState<T>.loading());
1718

1819
/// Creates a new instance of [Repo].
@@ -55,4 +56,8 @@ abstract class Repo<T>
5556

5657
/// Retrieves an instance of the specified [Repo] type from the service locator.
5758
static Future<R> get<R extends Repo>() => GetIt.instance.getAsync<R>();
59+
60+
/// This will be ignored by the module's DI system, as repositories are always treated as async singletons.
61+
@override
62+
bool get singelton => true;
5863
}

test/module_test.dart

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,15 @@ void main() {
4444
final service = di.get<_FakeService>();
4545
expect(service.config, same(di.get<_TestConfig>()));
4646

47+
final singletonService = di.get<_SingletonFakeService>();
48+
expect(singletonService.config, same(di.get<_TestConfig>()));
49+
4750
final datasource = di.get<_FakeDatasource>();
4851
expect(datasource.config, same(di.get<_TestConfig>()));
4952

53+
final singletonDatasource = di.get<_SingletonFakeDatasource>();
54+
expect(singletonDatasource.config, same(di.get<_TestConfig>()));
55+
5056
final repo = await di.getAsync<_FakeRepo>();
5157
expect(repo.config, same(di.get<_TestConfig>()));
5258
expect(repo.initializeCallCount, greaterThanOrEqualTo(1));
@@ -56,6 +62,41 @@ void main() {
5662
await module.free();
5763
});
5864

65+
group('Injectables', () {
66+
test('marked as factory return a new instance per resolution', () async {
67+
final module = _TestModule(_ImportModule());
68+
await module.initialize();
69+
70+
final serviceA = di.get<_FakeService>();
71+
final serviceB = di.get<_FakeService>();
72+
expect(serviceA, isNot(same(serviceB)));
73+
74+
final datasourceA = di.get<_FakeDatasource>();
75+
final datasourceB = di.get<_FakeDatasource>();
76+
expect(datasourceA, isNot(same(datasourceB)));
77+
78+
await module.free();
79+
});
80+
81+
test(
82+
'marked as singelton return the same instance per resolution',
83+
() async {
84+
final module = _TestModule(_ImportModule());
85+
await module.initialize();
86+
87+
final serviceA = di.get<_SingletonFakeService>();
88+
final serviceB = di.get<_SingletonFakeService>();
89+
expect(serviceA, same(serviceB));
90+
91+
final datasourceA = di.get<_SingletonFakeDatasource>();
92+
final datasourceB = di.get<_SingletonFakeDatasource>();
93+
expect(datasourceA, same(datasourceB));
94+
95+
await module.free();
96+
},
97+
);
98+
});
99+
59100
test('Classes are not available after disposing module', () async {
60101
final importModule = _ImportModule();
61102
final module = _TestModule(importModule);
@@ -68,7 +109,9 @@ void main() {
68109
expect(repo.disposed, isTrue);
69110
expect(di.isRegistered<_ExternalDependency>(), isFalse);
70111
expect(di.isRegistered<_FakeService>(), isFalse);
112+
expect(di.isRegistered<_SingletonFakeService>(), isFalse);
71113
expect(di.isRegistered<_FakeDatasource>(), isFalse);
114+
expect(di.isRegistered<_SingletonFakeDatasource>(), isFalse);
72115
expect(di.isRegistered<_FakeRepo>(), isFalse);
73116
expect(di.get<_TestConfig>(), isA<_TestConfig>());
74117
});
@@ -90,11 +133,13 @@ class _TestModule extends Module<int, _TestConfig> {
90133
@override
91134
void bindServices(Bind<Service, _TestConfig> bind) {
92135
bind((config, resolver) => _FakeService(config));
136+
bind((config, resolver) => _SingletonFakeService(config));
93137
}
94138

95139
@override
96140
void bindDatasources(Bind<Datasource, _TestConfig> bind) {
97141
bind((config, resolver) => _FakeDatasource(config));
142+
bind((config, resolver) => _SingletonFakeDatasource(config));
98143
}
99144

100145
@override
@@ -173,6 +218,20 @@ class _FakeService extends Service {
173218
String get logTag => '_FakeService';
174219
}
175220

221+
class _SingletonFakeService extends Service {
222+
_SingletonFakeService(this.config);
223+
224+
final _TestConfig config;
225+
226+
@override
227+
bool get singelton => true;
228+
229+
@override
230+
Future<void> free() async {}
231+
@override
232+
String get logTag => '_SingletonFakeService';
233+
}
234+
176235
class _FakeDatasource extends Datasource {
177236
_FakeDatasource(this.config);
178237

@@ -184,6 +243,20 @@ class _FakeDatasource extends Datasource {
184243
String get logTag => '_FakeDatasource';
185244
}
186245

246+
class _SingletonFakeDatasource extends Datasource {
247+
_SingletonFakeDatasource(this.config);
248+
249+
final _TestConfig config;
250+
251+
@override
252+
bool get singelton => true;
253+
254+
@override
255+
Future<void> free() async {}
256+
@override
257+
String get logTag => '_SingletonFakeDatasource';
258+
}
259+
187260
class _FakeRepo extends Repo<int> {
188261
_FakeRepo(this.config) {
189262
onInitialize(() => initializeHookRan = true);

0 commit comments

Comments
 (0)