Utilizing Dependency Injection in Flutter
Implementing Inversion of Control uses the design pattern known as Dependency Injection (DI). It permits the development of dependent objects outside of a class and provides those objects in various ways to a class. The creation and binding of the dependent things are moved outside the class that depends on them using DI. This results in more flexibility, decoupling, and simpler testing.
In this article, we’ll look at a few simple instances of dependency injection in Flutter.
What would be the purpose of using DI?
I’m not going to get into the logic behind the pattern in detail, but think about the class that follows:
Code:
class CoffeeMachine { private Grinder grinder = new Grinder(); makeCoffee() { grinder.grindCoffeeBeans(); // Other steps to make coffee } }
The previous class has no fundamental flaws, but it does have two significant issues:
It is impossible to separate and test the CoffeeMachine class using a dummy Grinder.
It would not be able to switch between a Manual Grinder and an Electric Grinder without changing the consumer.
We can fix these problems easily by creating different instances of CoffeeMachine with different types of grinders, as follows:
class CoffeeMachine { // Instance of the Grinder class final Grinder _grinder; // Constructor for dependency injection CoffeeMachine(this._grinder); // Method to make coffee makeCoffee() { _grinder.grindCoffeeBeans(); // Other steps to make coffee } }
Following the first approach clarifies why a class with many dependents may quickly get out of control.
Flutter’s Dependency Injection
Since Dependency Injection is a simple paradigm, libraries frequently abstract it from developers. These Flutter libraries often use reflection (mirrors in Dart). But there are two problems with Flutter:
For performance-related reasons, mirrors are disabled.
It is impractical to send dependencies many layers down the tree due to the nested nature of widgets.
We’ll use the Inject library to solve these problems. Inject uses the following annotations:
@Injector: A builder or collection of modules is used to create an injector, an inversion of a control container.
@Module and @Provides: Define classes and methods that provide dependencies to your Flutter app development.
@Component: It is used for dependency injection to enable specific modules.
Installation
Since there is no package in the official repository, we must manually install it. As a git package is how I prefer to approach it, I’ll describe its dependencies in the pubspec.yaml file as follows:
Code:
dependencies: inject: git: url: https://github.com/google/inject.dart.git path: package/inject dev_dependencies: build_runner: ^1.3.0 inject_generator: git: url: https://github.com/google/inject.dart.git path: package/inject_generator
Usage
What features do we typically expect from a DI library? Let’s go over a few typical use cases:
Injection of class concrete
It may be as easy as this:
import 'package:inject/inject.dart'; @provide class TaskService { // implementation }
We can use it e.g. with Flutter widgets like this:
@provide class SomeWidget extends StatelessWidget { final TaskService _service; SomeWidget(this._service); }
Interface Injection
In the beginning, we must define an abstract class with an implementation class, such as:
// Abstract class for CustomerRepository abstract class CustomerRepository { Future<List> allUsers(); } // Implementation of CustomerRepository using Firestore class FirestoreCustomerRepository implements CustomerRepository { @override Future<List> allUsers() { // Implementation for fetching all users from Firestore } }
Now that we have dependencies, we can add them to our module:
import ‘package:inject/inject.dart’; @module class UsersServices { @provide CustomerRepository customerRepository() => FirestoreCustomerRepository(); }
Providers
What should we do if we require a provider to continually give us a new class instance rather than an instance of that class to be injected? Or what if having a concrete example in the constructor is preferable to lazily resolving the dependency? The fact that you may request a function returning the required instance, and it will be injected appropriately, is something I didn’t find in the documentation (well, because there isn’t any documentation at all).
Even so, we can define a helper type as follows:
typedef Provider = T Function();
You can also utilize it in classes:
@provide class SomeWidget extends StatelessWidget { final Provider _service; SomeWidget(this._service); void _someFunction() { final service = _service(); // use service } }
Assisted injection
Since there is no built-in power to inject objects that need only runtime arguments, we can use the typical factory pattern in this situation: create a factory class that takes all the compile-time dependencies and injects them, and then provide a factory method with a runtime argument to make the necessary instance.
Qualifiers, singletons, and asynchronous injection
The library does support all of this. The official example provides a helpful explanation.
Installing it up
The last step is to build an injector (also known as a Dagger component), for example, as follows:
import ‘main.inject.dart’ as g; @Injector(const [UsersServices, DateResultsServices]) abstract class Main { @provide MyApp get app; static Future
We can now describe our primary function as follows:
void main() async { var container = await Main.create( UsersServices(), DateResultsServices(), ); runApp(container.app); }
Running
We must utilize build runner to generate the necessary code because inject uses code generation. Use this command to:
Code:
flutter packages pub run build_runner build
However, one crucial point is to note: by default, the code is generated into the cache folder, which Flutter does not support (though a solution to this issue is being worked on). Therefore, we must include the file inject_generator.build.yaml with the following information:
builders: inject_generator: target: “:inject_generator” import: “package:inject_generator/inject_generator.dart” builder_factories: — “summarizeBuilder” — “generateBuilder” build_extensions: “.dart”: — “.inject.summary” — “.inject.dart” auto_apply: dependents build_to: source
Wrap up
Dependency injection (DI) is a powerful design pattern that can help you write better Flutter code. It promotes modularity, reusability, testability, and maintainability by decoupling your code from its dependencies.
DI can be complex to implement initially, but many resources are available to help you, including DI libraries such as GetIt and Provider. If you are new to DI or need assistance implementing it in your Flutter project, you may hire flutter expert from a leading Flutter app development company who is always ready to assist you.
Frequently Asked Questions (FAQs)
1. What is dependency injection in Flutter app development?
Dependency injection in Flutter is a design pattern that allows you to decouple your code by injecting dependencies into classes instead of creating them directly.
2. What is dependency injection in use?
Dependency injection is utilized when creating a loosely linked application or making a class independent of its dependencies. The reusability of code can be increased by using dependency injection. If an object’s usage is decoupled, more dependencies can be replaced without altering classes.
3. Why is dependency injection advantageous?
Decreased coupling between classes and their dependencies is a crucial advantage of dependency injection. Programs become more reusable, testable, and manageable by hiding the implementation details from clients.
Book Your Flutter Developer Now
Contemporary ventures
Recent blog
ready to get started?
Fill out the form below and we will be in touch soon!
"*" indicates required fields