Skip to content

Dependency Injection

With Reactter, managing objects becomes straightforward. You can create, delete, and access desired objects from a single centralized location, accessible from anywhere in your code, all thanks to Reactter’s robust dependency injection system.

Dependency injection offers several benefits.

  • Inversion of Control: It adheres to the principle of inversion of control, where the responsibility for object creation and management is delegated to Reactter. This results in improved code modularity, reusability, and testability.
  • Simplified Code: By offloading the responsibility of creating dependencies from individual classes, dependency injection simplifies code, allowing classes to focus more on their core functionality.

Reactter provides the following dependencies injection mechanisms:

How it works

Reactter manages the dependencies through a centralized mechanism. This core component serves as a central repository for registering, resolving, and providing dependencies across the app. To comprehend this mechanism thoroughly, let’s break down the process into five stages:

  1. Registration: This stage involves registering the dependency into Reactter’s context with the specified id and mode params.

    For this, you can use the following methods:

    The Lifecycle.registered event is emitted.

  2. Resolving: When there is a request for getting a dependency, Reactter gets it according to id and the mode through the registry. If the dependency with/without id is not yet created, Reactter initializes it based on registry(this condition doesn’t apply to find method).

    For this, you can use the following methods:

    The following events are fired(only if the dependency instance is created):

    • Lifecycle.created
    • Lifecycle.willMount(flutter_reactter only)
    • Lifecycle.didMount(flutter_reactter only)
  3. Usage: The dependency is then used across the app as needed.

    Some of these events may occur:

    • Lifecycle.willUpdate
    • Lifecycle.didUpdate
  4. Deleting: When the dependency with/without id is no longer required, Reactter deletes it.

    For this, you can use the following methods:

    The following events are fired:

    • Lifecycle.willUnmount(flutter_reactter only)
    • Lifecycle.didUnmount(flutter_reactter only)
    • Lifecycle.deleted
  5. Unregistration: When the dependency with/without id is no longer required and depending on mode, Reactter unregisters it.

    For this, you can use the following methods:

    The Lifecycle.unregistered event is emitted.

Example

To understand it better, we will return to the countdown example seen from the State Management page, but now using the dependency injection:

1
import 'package:reactter/reactter.dart';
2
import 'countdown.dart';
3
4
void main() async {
5
// Create an instance of the `Countdown` class
6
final countdown = Reactter.create(() => Countdown())!;
7
// Start the countdown
8
await countdown.run();
9
}
1
import 'package:reactter/reactter.dart';
2
import 'counter.dart';
3
4
/// A class that represents a countdown using a counter
5
class Countdown {
6
// Create an instance of the `Counter` class using the `UseDependency` hook
7
// and initialize it with an initial value of 10
8
final uCounter = UseDependency.create<Counter>(() => Counter(10));
9
10
// Get the instance of the `Counter` class
11
Counter get counter => uCounter.instance;
12
13
/// Start the countdown
14
Future<void> run() {
15
// Listen to the `didUpdate` event of the `counter` instance
16
// and print the current `value` of `count` each time it changes
17
Reactter.on(
18
counter,
19
Lifecycle.didUpdate,
20
(_, __) => print('Count: ${counter.count}'),
21
);
22
23
// Create a timer that decrements the `count` state by 1
24
// every second until it reaches 0
25
return Timer.periodic(Duration(seconds: 1), _countdown);
26
}
27
28
// Decrement the `count` state by 1 each time the timer ticks
29
// and delete the `Counter` instance when the count value reaches 0
30
void _countdown(Timer timer) {
31
counter.decrement();
32
33
if (counter.count == 0) {
34
timer.cancel();
35
Reactter.delete<Counter>();
36
}
37
}
38
}
1
import 'package:reactter/reactter.dart';
2
3
/// A class that represents a counter with a `count` state
4
class Counter {
5
final Signal<int> _count;
6
7
int get count => _count.value;
8
9
const Counter(int initialValue) : _count = Signal(initialValue);
10
11
void decrement() => _count.value -= 1;
12
}

In this example, we have create a countdown of 10 seconds, and when it reaches 0 , the Counter instance is deleted. But we will make a small tweak to change the countdown behavior.

main.dart
1
import 'package:reactter/reactter.dart';
2
import 'countdown.dart';
3
import 'counter.dart';
4
5
void main() async {
6
// Register the `Counter` class with an initial value of 20
7
Reactter.register(() => Counter(20));
4 collapsed lines
8
// Create an instance of the `Countdown` class
9
final countdown = Reactter.create(() => Countdown())!;
10
// Start the countdown
11
await countdown.run();
12
}

Now, the countdown will start from 20 and when it reaches 0 , the Counter instance is deleted. What happens is that the Counter instance is registered with an initial value of 20 , and when the Countdown instance is created, it uses the Counter instance registered.

Ok, but what if we want to use the Counter instance in another part of the code? Let’s look:

main.dart
1
import 'package:reactter/reactter.dart';
2
import 'countdown.dart';
3
import 'counter.dart';
4
5
void main() async {
6 collapsed lines
6
// Register the `Counter` class with an initial value of 20
7
Reactter.register(() => Counter(20));
8
// Create an instance of the `Countdown` class
9
final countdown = Reactter.create(() => Countdown())!;
10
// Start the countdown
11
await countdown.run();
12
// Get the instance of the `Counter` class
13
final counter = Reactter.get<Counter>();
14
// Try to print the current count value
15
print('Count: ${counter?.count ?? 'Counter instance not found'}');
16
}

In this case, the countdown will work as before, but when trying to get the Counter instance to print itsvalue, the ouput will be “Counter instance not found”. This occurs because Counter was registered as DependencyMode.builder(the default mode), so when it was deleted at the end of the countdown its registration was also deleted. If we want to get the Counter instance to print its value, we need to register using the DependencyMode.singleton mode.

Let’s now delve into the modes of dependency registration.

Dependency Modes

The mode with which a dependency is registered determines how it is managed by Reactter. There are three modes:

Builder

Builder is a ways to manage a dependency, which registers a builder function and creates the instance, unless it has already done so.

In builder mode, when the dependency tree no longer needs it, it is completely deleted, including unregistration (deleting the builder function).

Reactter identifies the builder mode as DependencyMode.builder and it’s using for default.

Factory

Factory is a ways to manage a dependency, which registers a builder function only once and creates the instance if not already done.

In factory mode, when the dependency tree no longer needs it, the instance is deleted and the builder function is kept in the register.

Reactter identifies the factory mode as DependencyMode.factory and to active it,set it in the mode argument of Reactter.register and Reactter.create , or use Reactter.lazyFactory , Reactter.factory .

Singleton

Singleton is a ways to manage a dependency, which registers a builder function and creates the instance only once.

The singleton mode preserves the instance and its states, even if the dependency tree stops using it.

Reactter identifies the singleton mode as DependencyMode.singleton and to active it, set it in the mode argument of Reactter.register and Reactter.create , or use Reactter.lazySingleton , Reactter.singleton .