In Flutter, DI is crucial for managing dependencies efficiently, especially as applications grow in complexity. It allows for the injection of mock dependencies during testing, ensuring focused and reliable tests. DI promotes the reuse of services and components, reducing redundancy and enhancing modularity. By using DI, developers can create scalable, maintainable, and testable applications more effectively.
This guide will equip you with the knowledge to implement DI in Flutter, leading to better-structured and more manageable projects.
>> Read more about Flutter coding:
- How to Use Flutter Bloc for State Management in Flutter Apps?
- The In-depth Guide for Mastering Navigation in Flutter
- Step-by-Step Tutorial to Master Flutter TextField Validation
Comparison: With and Without Dependency Injection
Dependency Injection (DI) is a design pattern that separates the creation of an object's dependencies from its behavior, allowing dependencies to be provided externally. This approach passes the required dependencies into a class through its constructor, properties, or methods.
Here is some quick comparison when our Flutter project uses and doesn’t use Dependency Injection:
Without Dependency Injection | With Dependency Injection | |
---|---|---|
Coupling | Classes directly create and manage their dependencies. This leads to tight coupling between the classes and their dependencies, making it difficult to change or replace dependencies without modifying the classes themselves. | Dependencies are provided to classes externally, usually through constructors, properties, or interfaces. This results in loose coupling, making classes less dependent on specific implementations of their dependencies. |
Testing | Direct dependency creation makes it challenging to isolate classes for testing. Mocking dependencies is cumbersome and often requires class modifications. | DI facilitates the injection of mock dependencies, making it easier to write and run unit tests. Classes can be tested in isolation by providing mock implementations of their dependencies. |
Flexibility | With dependencies hard-coded into classes, making changes to those dependencies or swapping them for alternatives (e.g., different implementations or mocks) is cumbersome and error-prone. | DI allows easy swapping of dependencies without modifying the classes, useful for different configurations (e.g., development, testing, production). |
Maintainability | As the application grows, managing dependencies within the classes becomes more complex, leading to a tangled and hard-to-maintain codebase. | By managing dependencies externally, the codebase becomes more modular and easier to maintain. |
Reusability | Classes with hard-coded dependencies are less reusable because they cannot be easily repurposed in different contexts where different dependencies are needed. | Classes designed with DI are more reusable. They can be used in different contexts with different dependencies injected, making them more versatile and adaptable. |
3 Methods to Implement Dependency Injection in Flutter
Dependency Injection is a crucial pattern for managing dependencies in Flutter applications, ensuring that your code is more maintainable, testable, and scalable. Flutter offers several popular DI methods and packages that facilitate this pattern, each with its own set of features and use cases. Among the most widely used are Provider, GetIt, and Riverpod.
In the following sections, we will provide an overview of these DI methods, explaining their functionalities and how they can be integrated into your Flutter projects to improve code quality and development efficiency.
Using the Provider Package
The Provider package is a popular and powerful tool for dependency injection and state management in Flutter. It simplifies the process of passing data down the widget tree and allows for efficient and easy state management. Provider
is built on top of the InheritedWidget
and offers a clean and simple syntax to manage dependencies and state across your Flutter application.
- Step 1: Setting Up Provider
To use the Provider package, add it to your *pubspec.yaml*
file:
dependencies:
flutter:
sdk: flutter
provider: ^6.1.2
Or use the Flutter CLI to add the latest package version:
flutter pub add provider
- Step 2: Creating Mock Services
Now let’s create two simple mock services class (ApiService
and LoggingService
) to represent the services that need to inject to our project:
class ApiService {
String fetchData() {
return "Data fetched from API";
}
}
class LoggingService {
void log(String message) {
print("Log: $message");
}
}
- Step 3: Injecting Services with MultiProvider
This ApiService
will return a simple String
and the LoggingService
will print a message we provide. So, we can focus on implementing Dependency Injection with Provider for our project.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'services/api_service.dart';
import 'services/logging_service.dart';
void main() {
runApp(
MultiProvider(
providers: [
Provider<ApiService>(create: (_) => ApiService()),
Provider<LoggingService>(create: (_) => LoggingService()),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final apiService = Provider.of<ApiService>(context);
final loggingService = Provider.of<LoggingService>(context);
// Log a message using the LoggingService
loggingService.log("Fetching data from API");
return Scaffold(
appBar: AppBar(
title: Text("Using Provider with Two Services"),
),
body: Center(
child: Text(apiService.fetchData()),
),
);
}
}
In this example, both ApiService
and LoggingService
are injected into the widget tree using the Provider package. The MyHomePage
widget then consumes both services, fetching data from ApiService
and logging a message using LoggingService
. This approach demonstrates how to manage multiple dependencies efficiently with the Provider
package in a Flutter application.
Using the GetIt Package
Alongside Provider, which mainly focuses on reducing boilerplate code and managing state, the GetIt package is designed specifically for Dependency Injection (DI) in Flutter. While Provider manages and consumes data using a provider class as the source of truth for your app's state, DI is not its primary purpose. For a more focused approach to DI, GetIt is highly effective.
Using the GetIt package for DI in Flutter involves registering your services and accessing them from anywhere in your application. GetIt acts as a simple Service Locator for Dart and Flutter projects and can replace InheritedWidget
or Provider
for accessing objects from your UI.
- Step 1: Setting Up GetIt
To use the GetIt package, add it to your pubspec.yaml
file:
dependencies:
flutter:
sdk: flutter
get_it: ^7.2.0
Alternatively, you can use the Flutter CLI to add the latest version of the package:
flutter pub add get_it
- Step 2: Registering Services
We will use the same ApiService
and LoggingService
as in the Provider example. So, no need to have any update on it, we will only need to update in our main method as follow:
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'services/api_service.dart';
import 'services/logging_service.dart';
final GetIt getIt = GetIt.instance;
void setup() {
getIt.registerSingleton<ApiService>(ApiService());
getIt.registerSingleton<LoggingService>(LoggingService());
}
void main() {
setup();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
We can see here in the main method, we now add a setup method to inject two services into our application.
For simplicity, we will register our services as singletons (design pattern). But before we proceed, let’s take a quick look at the different ways get_it
can register dependencies:
- Singleton: means the instance of the object is created once and used all the way though out the life-cycle of our application, there are following methods:
registerSingleton
: Registers a singleton instance of the service. This instance is created immediately when registered.registerSingletonAsync
: Registers a singleton instance of the service that is created asynchronously. This is useful for services that require asynchronous initialization, such as those depending on I/O operations.registerLazySingleton
: Registers a singleton instance of the service that is created only when it is accessed for the first time. This can help improve startup performance by delaying the instantiation until it’s actually needed.registerLazySingletonAsync
: Registers a lazy singleton instance of the service that is created asynchronously when it is accessed for the first time.
- Factory: means each time the instance of the object is created, it’s a new instance of that object which hold no data of other instances:
registerFactory
: Registers a factory function that creates a new instance of the service every time it is requested. This is useful for services that should not share state.registerFactoryParam
: Registers a factory function that accepts up to two parameters for creating instances of the service.
- Step 3: Using GetIt
That’s a quick look into the methods that the get_it
package provides to inject our dependencies. Now, let's update our MyHomePage
to see how to use access the injected dependencies:
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final apiService = GetIt.instance.get<ApiService>();
final loggingService = GetIt.instance.get<LoggingService>();
loggingService.log("Fetching data from API");
return Scaffold(
appBar: AppBar(
title: Text("Using GetIt"),
),
body: Center(
child: Text(apiService.fetchData()),
),
);
}
}
Using the Riverpod Package
Riverpod is a reactive caching and data-binding framework that simplifies state management and dependency injection in Flutter applications. It provides a safer and more robust way to handle state and dependencies compared to the traditional Provider
package. Riverpod
ensures that your providers are always in sync with the widget tree, making your application more maintainable and testable.
- Step 1: Setting Up Riverpod
Like two example above, let’s add the Riverpod
package into our example project:
We can either do it manually:
dependencies:
flutter:
sdk: flutter
riverpod: 2.5.1
or using Flutter CLI:
flutter pub add riverpod
- Step 2: Registering Services
We will still be using the same two services (ApiService
and LoggingService
) as the provider
and get_it
examples. Therefore, we will only need to update our main method and the MyHomePage
class:
final apiServiceProvider = Provider<ApiService>((ref) {
return ApiService();
});
final loggingServiceProvider = Provider<LoggingService>((ref) {
return LoggingService();
});
ProviderScope
so that we can access our dependencies:void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
- Step 4: Accessing Services in the Widget Tree
Now let’s access these services in our MyHomePage
:
class MyHomePage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final apiService = ref.read(apiServiceProvider);
final loggingService = ref.read(loggingServiceProvider);
// Log a message using the LoggingService
loggingService.log("Fetching data from API");
return Scaffold(
appBar: AppBar(
title: Text("Using Riverpod with Two Services"),
),
body: Center(
child: Text(apiService.fetchData()),
),
);
}
}
In this example, both ApiService
and LoggingService
are exposed via providers using Riverpod
. The MyHomePage
widget consumes these providers to fetch data and log a message. This approach ensures a clean separation of concerns and makes the codebase more maintainable and testable.
>> Read more: Clean Architecture Flutter: Build Robust and Scalable Apps
Comparing 3 Dependency Injection Methods Above
Pros and Cons
Method | Pros | Cons |
Provider |
|
|
GetIt |
|
|
Riverpod |
|
|
Use Cases for Each Method
- Provider: Ideal for small to medium-sized projects where simplicity and quick setup are key. Best for projects that do not require extensive scalability or complexity in dependency management.
- GetIt: Ideal for managing simple dependencies with minimal overhead. Suitable for scenarios requiring global access to dependencies without the need for passing them down the widget tree. Effective when performance is a priority and extensive state management integration is not required.
- Riverpod: Ideal for large and complex projects that demand a robust and scalable dependency injection solution. Best for projects with sophisticated state management needs. Despite its steeper learning curve, it provides powerful and modern dependency management capabilities.
- coding
- Mobile App Development