Flutter Dependency Injection Explained With Real Project Examples

 

Flutter Dependency Injection Explained With Real Project Examples



As Flutter applications grow, managing dependencies becomes one of the biggest architectural challenges.

Initially, most developers directly create objects inside widgets like this:

final apiService = ApiService();

or:

final authRepository = AuthRepository();

This works perfectly in small applications.

But once projects become larger, problems start appearing:

  • difficult testing

  • tightly coupled code

  • duplicated instances

  • poor scalability

  • difficult maintenance

I personally experienced this while working on scalable Flutter applications with:

  • authentication

  • APIs

  • Firebase

  • local database

  • push notifications

  • repositories

  • multiple modules

Managing dependencies manually quickly became messy.

That’s where Dependency Injection became extremely useful.

In this article, I’ll explain:

  • what Dependency Injection actually is

  • why Flutter apps need it

  • how it improves architecture

  • singleton vs factory

  • service locators

  • get_it usage

  • repository injection

  • real-world project structure

This guide focuses on practical Flutter development rather than theoretical definitions.


What Is Dependency Injection?

Dependency Injection (DI) is simply:

providing required objects from outside instead of creating them directly inside classes.

Instead of this:

class HomeScreen {

  final apiService = ApiService();
}

You inject the dependency:

class HomeScreen {

  final ApiService apiService;

  HomeScreen(this.apiService);
}

Now the class does not create the dependency itself.

This improves:

  • flexibility

  • testing

  • scalability


Why Dependency Injection Matters

Without DI:

  • classes become tightly coupled

  • replacing implementations becomes difficult

  • testing becomes painful

With DI:

  • dependencies become reusable

  • architecture becomes cleaner

  • mocking becomes easier

  • large projects scale better


Real-World Example

Imagine your application has:

  • authentication

  • payment gateway

  • API services

  • analytics

  • notifications

  • local database

Without DI:

  • objects are created repeatedly

  • dependency chains become messy

  • object lifecycle becomes difficult

Dependency Injection solves this systematically.


Understanding Tight Coupling

Example of tightly coupled code:

class ProductRepository {

  final Dio dio = Dio();
}

Problems:

  • repository controls dependency creation

  • difficult mocking

  • difficult replacement

  • hard testing


Better Approach

Inject dependency externally:

class ProductRepository {

  final Dio dio;

  ProductRepository(this.dio);
}

Now:

  • dependency becomes replaceable

  • testing becomes easier

  • architecture becomes cleaner


Dependency Injection vs Service Locator

Many developers confuse these concepts.


Dependency Injection

Dependency is passed directly.

Example:

LoginBloc(authRepository)

Service Locator

A global object locator provides dependencies.

Example:

getIt<AuthRepository>()

Flutter developers often use:

  • get_it

which is technically:

  • Service Locator pattern

but commonly used as DI in Flutter architecture.


Why get_it Is Popular

Flutter does not include built-in dependency injection like:

  • Angular

  • Spring Boot

So packages like:

  • get_it

  • injectable

  • riverpod

became popular.


Installing get_it

Add dependency:

dependencies:
  get_it: ^7.7.0

Basic Setup

Create:

injection_container.dart

Initialize get_it

final getIt = GetIt.instance;

void setupDependencies() {

  getIt.registerLazySingleton<ApiService>(
    () => ApiService(),
  );
}

Access Dependency

final apiService = getIt<ApiService>();

This provides the same instance globally.


Understanding Singleton

Singleton means:

only one instance exists throughout the app lifecycle.

Useful for:

  • Dio

  • Firebase services

  • repositories

  • local database

  • shared preferences


registerSingleton

Creates immediately.

getIt.registerSingleton<ApiService>(
  ApiService(),
);

registerLazySingleton

Creates only when needed.

getIt.registerLazySingleton<ApiService>(
  () => ApiService(),
);

Usually preferred.


registerFactory

Creates NEW instance every time.

getIt.registerFactory<LoginCubit>(
  () => LoginCubit(),
);

Useful for:

  • Cubits

  • Blocs

  • temporary objects


Real Project Dependency Structure

A scalable Flutter project often looks like:

core/
├── network/
├── services/
├── di/

features/
├── auth/
├── products/
├── cart/

Inside:

di/
└── injection_container.dart

All dependencies are registered centrally.


Repository Injection Example


API Service

class ApiService {

  final Dio dio;

  ApiService(this.dio);
}

Repository

class ProductRepository {

  final ApiService apiService;

  ProductRepository(this.apiService);
}

Cubit

class ProductCubit extends Cubit<ProductState> {

  final ProductRepository repository;

  ProductCubit(this.repository);
}

Dependency Registration

void setupDependencies() {

  getIt.registerLazySingleton(
    () => Dio(),
  );

  getIt.registerLazySingleton(
    () => ApiService(getIt()),
  );

  getIt.registerLazySingleton(
    () => ProductRepository(getIt()),
  );

  getIt.registerFactory(
    () => ProductCubit(getIt()),
  );
}

This becomes extremely scalable.


Why Dependency Injection Improves Testing

Testing becomes much easier because dependencies can be replaced with mocks.

Example:

final mockRepository = MockRepository();

final cubit =
ProductCubit(mockRepository);

Without DI:

  • mocking becomes difficult

With DI:

  • dependencies become replaceable easily


Dependency Injection + Clean Architecture

Dependency Injection works beautifully with:

  • Clean Architecture

  • Repository Pattern

  • BLoC/Cubit

Because:

  • layers remain decoupled

  • dependencies stay organized

  • testing becomes easier


Common Mistakes Developers Make


1. Registering Everything As Singleton

Not every class should be singleton.

Wrong for:

  • Cubits

  • temporary UI controllers


2. Creating Dependencies Inside Widgets

Wrong:

final repository = Repository();

inside UI classes.

Dependencies should remain centralized.


3. Circular Dependencies

Example:

ServiceA → ServiceB
ServiceB → ServiceA

Avoid this completely.


4. Overengineering Small Apps

Tiny apps may not need full DI setup.

Do not add unnecessary complexity.


Dependency Injection + BLoC

One of the most common Flutter setups today:

Cubit/BLoC
↓
Repository
↓
API Service
↓
Dio/Firebase

Dependency Injection manages all these layers cleanly.


injectable Package

Many developers combine:

  • get_it

  • injectable

injectable helps generate registration code automatically.

But initially:

  • learning plain get_it first is better.


My Preferred Setup

For scalable Flutter apps, I usually prefer:

Cubit + Clean Architecture + get_it

because:

  • architecture stays clean

  • dependencies stay organized

  • testing becomes easier

  • scaling becomes smoother


Is Riverpod Also Dependency Injection?

Yes, partially.

Riverpod handles:

  • state management

  • dependency injection

very efficiently.

This is one reason Riverpod became extremely popular.


Real Benefits I Personally Experienced

After implementing proper DI:

  • code became cleaner

  • debugging improved

  • testing became easier

  • dependencies stayed centralized

  • large modules became manageable

Especially in long-term projects.


When Should You Start Using Dependency Injection?

My recommendation:

Beginners

First understand:

  • Flutter basics

  • state management

  • architecture concepts

Then learn DI.


For Production Apps

Dependency Injection becomes highly recommended once apps involve:

  • multiple repositories

  • APIs

  • Firebase

  • authentication

  • scalable architecture


Final Thoughts

Dependency Injection is one of the most important architectural concepts in scalable Flutter development.

Initially it may feel unnecessary.

But once applications grow:

  • centralized dependency management

  • clean architecture

  • reusable services

  • proper testing

become extremely valuable.

The goal of Dependency Injection is not complexity.
The real goal is:

building maintainable and scalable applications.


FAQs

Is get_it dependency injection?

Technically it is a Service Locator.
But commonly used as DI in Flutter.


Should beginners learn Dependency Injection?

Eventually yes.
But first understand Flutter fundamentals properly.


Is Riverpod better than get_it?

Riverpod handles both:

  • state management

  • dependency injection

while get_it focuses mainly on dependency management.


Is Dependency Injection necessary for small apps?

Usually no.

But for medium-to-large apps:

  • highly recommended.


Conclusion

In this article, we explored:

  • Flutter Dependency Injection

  • get_it

  • singleton vs factory

  • repository injection

  • scalable architecture

  • testing benefits

  • Clean Architecture integration

Dependency Injection helps Flutter applications become:

  • cleaner

  • more maintainable

  • more scalable

  • easier to test

  • easier to manage over time.

Comments

Popular Posts