Before continuing with this article, make sure you have clear understanding of what dependency is and what dependency injection is.
As your Flutter app grows, managing services like APIs, storage, authentication, and business logic gets harder and requires a cleaner way to inject all dependencies into your app without having to manually passing objects through constructors or widget trees.
This is where GetIt
comes in. It is a simple and very powerful service locator package and makes dependency injection clean, centralized and effortless. This results in:
- Cleaner code
- Easier testing
- Better scalability
Why Dependency Injection?
Dependency injection separates the concerns in your app and enables you to centrally inject dependencies without having to pass them for dependent classes separately.
Dependency Injection-The Common Ways
Most commonly used ways to inject dependencies in flutter are through constructor and setter, its simple and easy to understand and useful in small scale projects. For understanding purposes, we will consider an example of ApiService
class as mentioned below:
class ApiService {
void fetchData() {
print('Fetching data...');
}
}
Now let’s look at constructor injection and setter injection with the help of this ApiService
class.
Constructor Injection
As the name mentions, constructor injection refers to the process of passing dependencies through the class constructors. Consider below example for better understanding:
class HomeController {
final ApiService apiService;
// Constructor injection
HomeController(this.apiService);
}
void main() {
final apiService = ApiService(); // Create dependency
final controller = HomeController(apiService); // Inject via constructor
}
As you can see in above example, the dependency ApiService
has been passed to the HomeController
using the constructor of the HomeController
class.
Setter Injection
Setter injection is the process of passing dependencies through the use of setter. Consider below example for better understanding:
class HomeController {
ApiService? _apiService;
// Setter injection
set apiService(ApiService service) {
_apiService = service;
}
}
void main() {
final apiService = ApiService();
final controller = HomeController();
controller.apiService = apiService; // Inject after construction
}
In above example, setter apiService
is used to pass the dependency to HomeController
.
Now that we have seen common ways of dependency injections, let’s now go through dependency injection using GetIt
package in a step by step guide.
Step 1: Add get_it
to Your Project
Add get_it: latest_version
into your project’s pubspec.yaml
file like below:
dependencies:
get_it: ^7.6.0
Step 2: Create and Register Services
Let’s suppose we have an API service
class ApiService {
void fetchData() {
print('Fetching data...');
}
}
Now let’s create a service locator:
import 'package:get_it/get_it.dart';
final getIt = GetIt.I;
void setupLocator() {
getIt.registerLazySingleton<ApiService>(() => ApiService());
}
Now call this service locator in your main()
before runApp()
void main() {
setupLocator();
runApp(MyApp());
}
Step 3: Use the Service Anywhere
Now you can use injected ApiService
anywhere in your project using GetIt.I.<ApiService>()
without having to reinstantiate it. Following is an example of it:
class HomeScreen extends StatelessWidget {
final api = GetIt.I.get<ApiService>();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () => api.fetchData(),
child: Text('Fetch Data'),
),
),
);
}
}
Overview of GetIt
Registration Methods
Below is a table of main registration methods available in GetIt
:
Method |
Description |
Usage |
---|---|---|
|
Registers an already created instance. Instantly available. |
Lightweight services needed immediately (e.g. config). |
|
Registers a factory function. Instance created on first access, then reused. |
Heavy or rarely used services (e.g. DB, network). |
|
Returns a new instance every time |
BLoCs, controllers, or anything short-lived. |
|
Registers an async factory. Instance is awaited during |
Services that require async setup (e.g. reading from disk). |
|
Returns a new async instance each time. |
Fresh async instances per use (less common). |
Testing with Dependency Injection
As mentioned earlier, dependency injection makes it easier to test our code as well, we will see it in action now. Suppose we have a controller that depends on ApiService()
:
class HomeController {
final ApiService apiService;
HomeController(this.apiService);
}
For testing, we will use a mock API service that will extend from ApiService()
:
class MockApiService extends ApiService {
@override
void fetchData() {
print('Mock fetch');
}
}
void main() {
final controller = HomeController(MockApiService());
controller.apiService.fetchData();
}
No need to modify ApiService() code, passing dependency through constructor will run our tests just fine.
Conventional Methods vs GetIt
Feature |
Conventional DI |
GetIt |
---|---|---|
Dependency visibility |
Clear |
Hidden (global lookup) |
Testability |
Very good |
Needs more setup |
Boilerplate |
Higher |
Minimal |
Global access |
No |
Yes |
Scales well |
No (without extra tools) |
Yes |
Null safety |
Strong |
Depends on usage |
External packages needed |
No |
Yes ( |
Lazy/singleton handling |
Manual |
Built-in |
When to Use Which?
Conventional dependency injection methods work best when:
- Testability is priority
- App is small scale
- You don’t want to rely on third party packages
GetIt
works best when:
- App is growing big in size and complexity
- There are many global dependencies to rely upon
- Want to keep widget tree clean by avoiding passing dependencies through constructors
Best Practices & Tips
- Don’t register everything in GetIt, only shared dependencies
- Prefer using
registerFactory
when using transient logic like BLOC - Don’t forget to call
GetIt.I.reset()
in test setup for isolation
Final Thoughts
Dependency injection plays a key role in making your code scalable, testable and easy to manage. It cleans up your classes by separating configurations and logic and when using a powerful service locator like GetIt
, your project’s dependencies become centrally managed and this makes you bypass having to pass the dependencies through constructors as you have access to the dependencies globally. Additionally, it increases the code testability.
In summary, combining dependency injection principles with GetIt
not only promotes cleaner architecture but also boosts development speed, enhances modularity, and prepares your codebase for long term growth.