Skip to content

Dependency injection

Dependency injection is a design pattern that simplifies the management of an object’s dependencies.

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

Dependency injection offers several benefits. Some of the most notable are:

  • Decoupling: Dependency injection decouples classes from their dependencies, making it easier to modify and reuse code.
  • Inversion of control: It adheres to the principle of inversion of control, where the responsibility of creating and managing objects is delegated to Reactter. This results in better modularity, reusability, and testability of the code.
  • Simplified code: By delegating the responsibility of creating dependencies from individual classes, dependency injection simplifies the code, allowing classes to focus more on their core functionality.

API

Reactter provides the following dependencies injection mechanisms:

How It Works

Reactter manages dependencies through a centralized mechanism that acts as a main repository responsible for registering, resolving, and providing dependencies throughout the application. To understand how this system works in its entirety, we will break down the process into the following stages:

  1. Registration: This stage involves registering the dependency within the Reactter context using a specific type, a builder function, an id, and a dependency mode.

    To perform this registration, you can use the following methods:

    During registration, the event Lifecycle.registered is triggered.

  2. Resolution: When a dependency is requested, Reactter creates an instance of the dependency from the registered builder function, according to the provided type and id.

    For this, you can use the following methods:

    If a new dependency instance is created, the following events will be emitted:

    • Lifecycle.created
    • Lifecycle.willMount (only in flutter_reactter)
    • Lifecycle.didMount (only in flutter_reactter)
  3. Usage: Once the dependency is resolved, its instance can be used anywhere within the application.

    To access the dependency or check its state, you can use the following methods:

    If the dependency’s state is updated, the following events will be emitted:

    • Lifecycle.willUpdate
    • Lifecycle.didUpdate
  4. Deletion: In this stage, Reactter removes the dependency instance based on the provided type and id.

    To do this, you can use the following methods:

    During deletion, the following events will be emitted:

    • Lifecycle.willUnmount (only in flutter_reactter)
    • Lifecycle.didUnmount (only in flutter_reactter)
    • Lifecycle.deleted
  5. Unregistration: In this stage, Reactter removes the dependency’s registration based on the provided type and id.

    To unregister the dependency, you can use the following methods:

    When the dependency registration is removed, the event Lifecycle.unregistered will be emitted.

Example

To better understand this, let’s go back the countdown example from the state manager page, but now using 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 = Rt.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 representing a countdown
5
class Countdown {
6
// Create an instance of the 'Counter' class using the 'UseDependency' hook
7
// with an initial value of 10
8
final uCounter = UseDependency.create<Counter>(() => Counter(10));
9
10
// Get the 'Counter' instance
11
Counter get counter => uCounter.instance;
12
13
/// Start the countdown
14
Future<void> run() {
15
// Listen for the 'didUpdate' event on the 'counter' instance
16
// and print the current value of 'count'
17
Rt.on(
18
counter,
19
Lifecycle.didUpdate,
20
(_, __) => print('Count: ${counter.count}'),
21
);
22
23
// Create a timer that calls the '_countdown' function
24
// every second
25
return Timer.periodic(Duration(seconds: 1), _countdown);
26
}
27
28
// Decrement the 'count' state by 1 every timer cycle
29
// and delete the 'Counter' instance when the value reaches 0
30
void _countdown(Timer timer) {
31
counter.decrement();
32
33
if (counter.count == 0) {
34
timer.cancel();
35
Rt.delete<Counter>();
36
}
37
}
38
}
1
import 'package:reactter/reactter.dart';
2
3
/// A class representing a counter that holds the '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’ve created a countdown from 10 seconds, and when it reaches 0 , the Counter instance is deleted. But if we want to use the Counter instance elsewhere in the code, we can do it like this:

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
Rt.register(() => Counter(20));
4 collapsed lines
8
// Create an instance of the 'Countdown' class
9
final countdown = Rt.create(() => Countdown())!;
10
// Start the countdown
11
await countdown.run();
12
}

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

Ok, But what if we want to use the instance of Counter elsewhere in the code? Let’s see:

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
Rt.register(() => Counter(20));
8
// Create an instance of the `Countdown` class
9
final countdown = Rt.create(() => Countdown())!;
10
// Start the countdown
11
await countdown.run();
12
// Get the instance of the `Counter` class
13
final counter = Rt.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 instance of Counter to print its value, the output will be ‘Counter instance not found’ . This happens because Counter was registered as DependencyMode.builder (the default mode), so when it is deleted at the end of the countdown, its registration is also removed.

If we want to obtain the instance of Counter to print its value, we need to register it using the DependencyMode.singleton mode, as shown below:

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 as singleton mode with an initial value of 20
7
Rt.register(() => Counter(20), mode: DependencyMode.singleton);
8 collapsed lines
8
// Create an instance of the `Countdown` class
9
final countdown = Rt.create(() => Countdown())!;
10
// Start the countdown
11
await countdown.run();
12
// Get the instance of the `Counter` class
13
final counter = Rt.get<Counter>();
14
// Try to print the current `count` value
15
print('Count: ${counter?.count ?? 'Counter instance not found'}');
16
}

Dependency Modes

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

Builder

The Builder mode is a way to manage a dependency by registering a builder function and creating an instance only if it hasn’t been created previously.

In this mode, when the dependency tree no longer needs it, the instance is completely removed, including the registration and the builder function.

Reactter identifies the Builder mode as DependencyMode.builder and uses it by default.

Factory

The Factory mode is a way to manage a dependency in which a builder function is registered, and a new instance is created every time it is requested.

In this mode, when the dependency tree no longer uses it, the instance is removed, but the builder function remains registered.

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

Singleton

The Singleton mode is a way to manage a dependency by registering a builder function and ensuring that the instance is created only once.

When using the singleton mode, the dependency instance and its states remain active, even if the dependency tree no longer uses it. This also includes the creation function, unless it is explicitly removed.

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