Before continuing with this article, make sure you have clear understanding of what dependency is and what dependency injection is. 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: GetIt Cleaner code Easier testing Better scalability 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: ApiService class ApiService { void fetchData() { print('Fetching data...'); } } class ApiService { void fetchData() { print('Fetching data...'); } } Now let’s look at constructor injection and setter injection with the help of this ApiService class. ApiService 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); } class HomeController { final ApiService apiService; // Constructor injection HomeController(this.apiService); } void main() { final apiService = ApiService(); // Create dependency final controller = HomeController(apiService); // Inject via constructor } 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. ApiService HomeController HomeController 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; } } 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 } 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. apiService 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. GetIt Step 1: Add get_it to Your Project get_it Add get_it: latest_version into your project’s pubspec.yaml file like below: get_it: latest_version pubspec.yaml dependencies: get_it: ^7.6.0 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...'); } } 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()); } 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() main() runApp() void main() { setupLocator(); runApp(MyApp()); } 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: ApiService GetIt.I.<ApiService>() 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'), ), ), ); } } 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 GetIt Below is a table of main registration methods available in GetIt: GetIt Method Description Usage registerSingleton<T>(T) Registers an already created instance. Instantly available. Lightweight services needed immediately (e.g. config). registerLazySingleton<T>() Registers a factory function. Instance created on first access, then reused. Heavy or rarely used services (e.g. DB, network). registerFactory<T>() Returns a new instance every time get<T>() is called. BLoCs, controllers, or anything short-lived. registerSingletonAsync<T>() Registers an async factory. Instance is awaited during get<T>(). Services that require async setup (e.g. reading from disk). registerFactoryAsync<T>() Returns a new async instance each time. Fresh async instances per use (less common). Method Description Usage registerSingleton<T>(T) Registers an already created instance. Instantly available. Lightweight services needed immediately (e.g. config). registerLazySingleton<T>() Registers a factory function. Instance created on first access, then reused. Heavy or rarely used services (e.g. DB, network). registerFactory<T>() Returns a new instance every time get<T>() is called. BLoCs, controllers, or anything short-lived. registerSingletonAsync<T>() Registers an async factory. Instance is awaited during get<T>(). Services that require async setup (e.g. reading from disk). registerFactoryAsync<T>() Returns a new async instance each time. Fresh async instances per use (less common). Method Description Usage Method Method Description Description Usage Usage registerSingleton<T>(T) Registers an already created instance. Instantly available. Lightweight services needed immediately (e.g. config). registerSingleton<T>(T) registerSingleton<T>(T) registerSingleton<T>(T) Registers an already created instance. Instantly available. Registers an already created instance. Instantly available. Lightweight services needed immediately (e.g. config). Lightweight services needed immediately (e.g. config). registerLazySingleton<T>() Registers a factory function. Instance created on first access, then reused. Heavy or rarely used services (e.g. DB, network). registerLazySingleton<T>() registerLazySingleton<T>() registerLazySingleton<T>() Registers a factory function. Instance created on first access, then reused. Registers a factory function. Instance created on first access, then reused. on first access Heavy or rarely used services (e.g. DB, network). Heavy or rarely used services (e.g. DB, network). registerFactory<T>() Returns a new instance every time get<T>() is called. BLoCs, controllers, or anything short-lived. registerFactory<T>() registerFactory<T>() registerFactory<T>() Returns a new instance every time get<T>() is called. Returns a new instance every time get<T>() is called. new instance get<T>() BLoCs, controllers, or anything short-lived. BLoCs, controllers, or anything short-lived. registerSingletonAsync<T>() Registers an async factory. Instance is awaited during get<T>(). Services that require async setup (e.g. reading from disk). registerSingletonAsync<T>() registerSingletonAsync<T>() registerSingletonAsync<T>() Registers an async factory. Instance is awaited during get<T>(). Registers an async factory. Instance is awaited during get<T>(). async get<T>() Services that require async setup (e.g. reading from disk). Services that require async setup (e.g. reading from disk). registerFactoryAsync<T>() Returns a new async instance each time. Fresh async instances per use (less common). registerFactoryAsync<T>() registerFactoryAsync<T>() registerFactoryAsync<T>() Returns a new async instance each time. Returns a new async instance each time. new async instance Fresh async instances per use (less common). 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(): ApiService() class HomeController { final ApiService apiService; HomeController(this.apiService); } class HomeController { final ApiService apiService; HomeController(this.apiService); } For testing, we will use a mock API service that will extend from ApiService(): ApiService() class MockApiService extends ApiService { @override void fetchData() { print('Mock fetch'); } } void main() { final controller = HomeController(MockApiService()); controller.apiService.fetchData(); } 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 (get_it) Lazy/singleton handling Manual Built-in 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 (get_it) Lazy/singleton handling Manual Built-in Feature Conventional DI GetIt Feature Feature Conventional DI Conventional DI GetIt GetIt Dependency visibility Clear Hidden (global lookup) Dependency visibility Dependency visibility Clear Clear Hidden (global lookup) Hidden (global lookup) Testability Very good Needs more setup Testability Testability Very good Very good Needs more setup Needs more setup Boilerplate Higher Minimal Boilerplate Boilerplate Higher Higher Minimal Minimal Global access No Yes Global access Global access No No Yes Yes Scales well No (without extra tools) Yes Scales well Scales well No (without extra tools) No (without extra tools) Yes Yes Null safety Strong Depends on usage Null safety Null safety Strong Strong Depends on usage Depends on usage External packages needed No Yes (get_it) External packages needed External packages needed No No Yes (get_it) Yes (get_it) get_it Lazy/singleton handling Manual Built-in Lazy/singleton handling Lazy/singleton handling Manual Manual Built-in 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 Testability is priority App is small scale You don’t want to rely on third party packages GetIt works best when: GetIt 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 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 Don’t register everything in GetIt, only shared dependencies Prefer using registerFactory when using transient logic like BLOC registerFactory Don’t forget to call GetIt.I.reset() in test setup for isolation GetIt.I.reset() 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. GetIt 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. GetIt