Dependency Injection in Flutter

Prashant BalePrashant Bale
4 min read

Imagine you're building a toy robot. Every time you want to build one, you go hunting for its batteries, wheels, and sensors. Sounds tiring, right? But what if someone hands you a toolbox with all the parts neatly organized? Now it’s easy!

That’s Dependency Injection (DI) — instead of every class finding or creating its own parts (dependencies), they’re handed in from the outside. Clean, simple, and organized!


What is Dependency Injection?

Dependency Injection is a design pattern where a class gets the objects it depends on — called dependencies — from outside, rather than creating them internally.

Let’s say you have a LoginService class that needs ApiService to work. Without DI, it would create its own ApiService like this:

class LoginService {
  final ApiService apiService = ApiService(); // tightly coupled
}

This creates a strong dependency, making testing or replacing ApiService difficult.

With DI, you pass it from outside:

class LoginService {
  final ApiService apiService;

  LoginService(this.apiService); // dependency injected from outside
}

Now, LoginService doesn’t care where ApiService comes from. You can swap, mock, or upgrade it easily!


Why Should You Use Dependency Injection?

Let’s break down some powerful benefits:

  • Testability: You can inject mock objects for testing.

  • Loose Coupling: Classes don’t depend on specific implementations.

  • Flexibility: Easier to switch or update logic later.

  • Readability and Structure: A clear flow of how data moves through your app.


Ways to Use Dependency Injection in Flutter

Flutter doesn’t have built-in DI, but there are amazing tools like Provider, get_it, and manual constructor injection.

Let’s walk through them one by one.


1. Constructor Injection (Simple & Manual)

Constructor injection is the simplest and most straightforward way to inject dependencies. You just pass the dependency into the constructor.

class ApiService {
  void fetchUser() => print("User Fetched!");
}

class UserRepository {
  final ApiService apiService;

  UserRepository(this.apiService);

  void loadUser() {
    apiService.fetchUser();
  }
}

In the main() function, we create the dependency and inject it:

void main() {
  final api = ApiService();
  final repo = UserRepository(api);
  repo.loadUser();
}

Explanation:

  • UserRepository depends on ApiService.

  • Instead of creating ApiService inside UserRepository, we inject it.

  • Makes it easy to replace ApiService with a mock or alternative later.


2. Using Provider — Flutter’s Favorite

The provider package is a popular state management and DI tool. It uses Flutter’s InheritedWidget under the hood to inject dependencies throughout the widget tree.

Step 1: Add Dependency

dependencies:
  provider: ^6.1.0

Step 2: Create Services

class ApiService {
  String fetchData() => "Data from API";
}
class DataRepository {
  final ApiService apiService;
  DataRepository(this.apiService);

  String getData() => apiService.fetchData();
}

Step 3: Inject with MultiProvider

void main() {
  runApp(
    MultiProvider(
      providers: [
        Provider<ApiService>(create: (_) => ApiService()),
        ProxyProvider<ApiService, DataRepository>(
          update: (_, apiService, __) => DataRepository(apiService),
        ),
      ],
      child: MyApp(),
    ),
  );
}

Step 4: Access in Your Widgets

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final repository = Provider.of<DataRepository>(context);
    final data = repository.getData();

    return Scaffold(
      appBar: AppBar(title: Text("DI Example")),
      body: Center(child: Text(data)),
    );
  }
}

Explanation:

  • ApiService is registered at the top using Provider.

  • DataRepository is injected using ProxyProvider because it depends on ApiService.

  • Inside HomeScreen, we access DataRepository via Provider.of.

This is clean and perfect for large apps!


3. Using get_it — The Flutter Service Locator

get_it is another lightweight and elegant way to do DI. It lets you register and retrieve services from a global service locator.

Step 1: Add the Package

dependencies:
  get_it: ^7.6.4

Step 2: Register Services

import 'package:get_it/get_it.dart';

final getIt = GetIt.instance;

void setup() {
  getIt.registerLazySingleton<ApiService>(() => ApiService());
  getIt.registerLazySingleton<DataRepository>(
    () => DataRepository(getIt<ApiService>()),
  );
}

Step 3: Initialize Before Running the App

void main() {
  setup(); // register everything
  runApp(MyApp());
}

Step 4: Access Anywhere

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final repository = getIt<DataRepository>();
    final data = repository.getData();

    return Scaffold(
      appBar: AppBar(title: Text("GetIt Example")),
      body: Center(child: Text(data)),
    );
  }
}

Explanation:

  • Services are registered once in setup().

  • getIt<T>() gives you the instance whenever and wherever you want.

  • It’s quick, global, and super convenient — especially for small to medium apps.


Best Practices for Dependency Injection in Flutter

  • Use constructor injection when possible.

  • Use Provider for scalable, reactive apps.

  • Use get_it for simpler service locator needs.

  • Keep the setup centralized and organized.

  • Write unit tests with mocked services — that’s the real win of DI!


Summary

MethodProsWhen to Use
ConstructorSimple, testableSmall projects, basic apps
ProviderReactive, integrates with widgetsMedium to large Flutter apps
get_itEasy, fast global accessSmall to medium apps, quick setup

By using DI, you’re giving your classes the superpower of independence. Whether you're building a small app or a large enterprise project, injecting dependencies keeps your code flexible, testable, and maintainable.

Happy Coding !!

0
Subscribe to my newsletter

Read articles from Prashant Bale directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Prashant Bale
Prashant Bale

With 17+ years in software development and 14+ years specializing in Android app architecture and development, I am a seasoned Lead Android Developer. My comprehensive knowledge spans all phases of mobile application development, particularly within the banking domain. I excel at transforming business needs into secure, user-friendly solutions known for their scalability and durability. As a proven leader and Mobile Architect.