flutter
/

Dependency Injection with Bloc: Managing Dependencies at Scale

Last Sync: Today

On this page

10
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Dependency Injection with Bloc: Managing Dependencies at Scale

Dependency injection (DI) is a fundamental technique for writing testable, maintainable code. In Bloc applications, you need to inject repositories, use cases, and other dependencies into your blocs. This guide covers the most effective DI approaches for Flutter Bloc, from simple BlocProvider to advanced setups with get_it and injectable.

Why Dependency Injection Matters

  • Testability – Dependencies can be mocked or replaced in tests.
  • Flexibility – Swap implementations (e.g., fake API vs real API) without changing blocs.
  • Separation of concerns – Blocs don't know how dependencies are created.
  • Reusability – Same service instance can be shared across multiple blocs.
  • Lazy initialisation – Create dependencies only when needed.

DI Options in Flutter Bloc

ApproachUse CaseProsCons
`BlocProvider` / `RepositoryProvider`Simple apps, widget‑scoped dependenciesBuilt‑in, automatic disposalLimited to widget tree, no global access
`get_it` (service locator)Medium to large apps, global servicesEasy to use, fast access, testableService locator is a pattern, not true DI
`injectable` + `get_it`Large apps, code‑generated registrationType‑safe, minimal boilerplate, great for teamsRequires code generation setup

Approach 1: BlocProvider and RepositoryProvider

BlocProvider is not only for providing blocs – you can also use RepositoryProvider to inject repositories or services into the widget tree. This approach ties dependencies to the widget lifecycle and is ideal for simple apps.

DARTRead-only
1
MultiRepositoryProvider(
  providers: [
    RepositoryProvider<AuthRepository>(
      create: (context) => AuthRepositoryImpl(
        remoteDataSource: AuthRemoteDataSourceImpl(),
      ),
    ),
    RepositoryProvider<SettingsRepository>(
      create: (context) => SettingsRepositoryImpl(),
    ),
  ],
  child: MultiBlocProvider(
    providers: [
      BlocProvider<AuthBloc>(
        create: (context) => AuthBloc(
          authRepository: context.read<AuthRepository>(),
        ),
      ),
    ],
    child: MyApp(),
  ),
)

Approach 2: get_it – The Simple Service Locator

get_it is the most popular service locator in the Flutter ecosystem. It allows you to register dependencies globally and access them anywhere, without being tied to the widget tree.

YAMLRead-only
1
dependencies:
  get_it: ^7.6.0
DARTRead-only
1
// lib/di/injection.dart
import 'package:get_it/get_it.dart';

final getIt = GetIt.instance;

Future<void> setup() async {
  // Register singletons
  getIt.registerSingleton<HttpClient>(HttpClientImpl());
  
  // Register lazy singletons (created when first accessed)
  getIt.registerLazySingleton<AuthRemoteDataSource>(
    () => AuthRemoteDataSourceImpl(getIt<HttpClient>()),
  );
  getIt.registerLazySingleton<AuthRepository>(
    () => AuthRepositoryImpl(getIt<AuthRemoteDataSource>()),
  );
  
  // Register factories (new instance each time)
  getIt.registerFactory<AuthBloc>(
    () => AuthBloc(getIt<AuthRepository>()),
  );
  
  // Register with parameters
  getIt.registerFactoryParam<UserBloc, String, void>(
    (userId, _) => UserBloc(userId, getIt<UserRepository>()),
  );
}

// Use in code:
final authBloc = getIt<AuthBloc>();

You can combine get_it with BlocProvider – use get_it to create the bloc, then provide it via BlocProvider to ensure proper disposal.

DARTRead-only
1
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider<AuthBloc>(
          create: (_) => getIt<AuthBloc>(),
        ),
        BlocProvider<HomeBloc>(
          create: (_) => getIt<HomeBloc>(),
        ),
      ],
      child: MaterialApp(...),
    );
  }
}

Approach 3: injectable – Code‑Generated DI

injectable is a code‑generator that works on top of get_it. It automatically creates the registration code based on annotations, reducing boilerplate and preventing registration errors.

YAMLRead-only
1
dependencies:
  get_it: ^7.6.0
  injectable: ^2.3.0

dev_dependencies:
  build_runner: ^2.4.0
  injectable_generator: ^2.4.0
DARTRead-only
1
// lib/di/injection.dart
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';

final getIt = GetIt.instance;

@InjectableInit()
Future<void> configureDependencies() => getIt.init();

// lib/di/injection.config.dart – generated automatically
DARTRead-only
1
@singleton
class HttpClientImpl implements HttpClient { ... }

@lazySingleton
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { ... }

@injectable
class AuthRepositoryImpl implements AuthRepository { ... }

@injectable
class AuthBloc extends Bloc<AuthEvent, AuthState> {
  AuthBloc(@factoryParam String? initialEmail) : super(...);
}

// With parameters
@injectable
class UserBloc extends Bloc<UserEvent, UserState> {
  UserBloc(@factoryParam String userId, UserRepository repo) : super(...);
}
BASHRead-only
1
flutter pub run build_runner build

Scoping and Lifecycle

Different dependencies have different lifetimes. Here’s how to manage scoping:

  • App‑wide singletons – Use registerSingleton (get_it) or @singleton (injectable). Examples: HTTP client, database, shared preferences.
  • Feature‑scoped – Use BlocProvider to create a new bloc when entering a feature and dispose it when leaving. Combine with get_it for other dependencies.
  • Screen‑scoped – Create a new instance of a bloc for a specific screen using BlocProvider without registering it globally.
  • Transient – Use registerFactory (get_it) or @injectable without singleton annotation to get a new instance each time.

Testing with Dependency Injection

DI makes testing straightforward. In your test setup, you can replace real dependencies with mocks.

DARTRead-only
1
import 'package:mocktail/mocktail.dart';
import 'package:get_it/get_it.dart';

class MockAuthRepository extends Mock implements AuthRepository {}

void main() {
  late MockAuthRepository mockRepo;

  setUp(() {
    mockRepo = MockAuthRepository();
    // Override the real repository with a mock
    getIt.registerSingleton<AuthRepository>(mockRepo);
  });

  tearDown(() {
    getIt.reset(); // Resets all registrations
  });

  test('AuthBloc calls login on repository', () async {
    when(() => mockRepo.login(any(), any()))
        .thenAnswer((_) async => User(...));

    final bloc = getIt<AuthBloc>();
    // ...
  });
}

Best Practices

  • Prefer get_it for infrastructure dependencies – Repositories, services, data sources – register them as singletons.
  • Use BlocProvider for blocs – Let the widget tree manage bloc lifecycle. Use get_it to create the bloc instance if needed.
  • Avoid using get_it directly inside widgets – Instead, use context.read/context.watch on BlocProvider where possible. Use get_it for services not tied to the UI.
  • Register with proper scope – Singletons for stateless services; factories for blocs or stateful objects.
  • Use injectable for larger projects – It enforces a consistent DI setup and reduces manual registration errors.
  • Reset DI container between tests – Use getIt.reset() in tearDown to avoid test pollution.

Common Mistakes

  • ❌ Registering everything as a singleton – Blocs registered as singletons retain state across screens, leading to bugs. Use factories for blocs.
  • ❌ Using get_it inside the build method for state – Won't cause rebuilds; use context.watch or BlocBuilder instead.
  • ❌ Not resetting get_it in tests – Causes tests to interfere with each other.
  • ❌ Creating circular dependencies – For example, a repository depending on a bloc, and the bloc depending on the repository. Break the cycle.
  • ❌ Over‑using DI for simple values – For simple parameters, constructor injection is fine; DI is overkill for every tiny object.

What's Next?

Now that your dependencies are well‑managed, explore how to structure your app with modular architecture and Clean Architecture.

Next, explore Modular architecture with Bloc and Repository pattern.

Test Your Knowledge

Q1
of 3

Which package is commonly used for code‑generated dependency injection in Flutter Bloc?

A
get_it
B
injectable
C
provider
D
bloc_provider
Q2
of 3

When should you use BlocProvider instead of get_it for a bloc?

A
When the bloc should be globally accessible
B
When the bloc should be scoped to a widget subtree and automatically disposed
C
When the bloc has no dependencies
D
When using injectable
Q3
of 3

How do you reset the get_it container between tests?

A
getIt.clear()
B
getIt.reset()
C
getIt.dispose()
D
GetIt.instance = GetIt()

Frequently Asked Questions

Should I use get_it or BlocProvider for dependency injection?

Both have their place. Use BlocProvider for blocs that should be scoped to the widget tree and automatically disposed. Use get_it for services and repositories that are app‑wide. It’s common to use both together.

How do I handle asynchronous dependencies (like SharedPreferences) with get_it?

Use registerSingletonAsync for async initialisation. Await the registration in your main function before running the app. Example: await getIt.registerSingletonAsync(() => SharedPreferences.getInstance());

Can I use get_it with BlocProvider to create a bloc that depends on a repository?

Yes. You can register the repository as a singleton in get_it, then create a factory for the bloc that depends on it. In BlocProvider, call getIt<MyBloc>() to get the instance.

What is the difference between get_it and Provider/InheritedWidget?

get_it is a service locator – you ask for dependencies by type, independent of the widget tree. Provider/BlocProvider use InheritedWidget to provide dependencies to a subtree. They serve different purposes and can be combined.

Previous

bloc multi bloc communication

Next

bloc getit

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.