flutter
/

Injectable with Bloc: Code‑Generated Dependency Injection

Last Sync: Today

On this page

9
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Injectable with Bloc: Code‑Generated Dependency Injection

Managing dependencies manually with get_it is straightforward, but as your app grows, registration code can become repetitive and error‑prone. Injectable is a code‑generator that automates dependency injection setup, working seamlessly with get_it and Bloc. This guide shows how to use injectable to write clean, maintainable DI code in your Bloc applications.

What is Injectable?

Injectable is a set of annotations and a code generator that creates the registration code for get_it. It allows you to mark classes with annotations like @injectable, @singleton, @lazySingleton, and the generator produces the registration file. This reduces boilerplate, ensures consistent registration, and makes refactoring safer.

Setup and Installation

YAMLRead-only
1
dependencies:
  flutter:
    sdk: flutter
  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();
BASHRead-only
1
flutter pub run build_runner build
# or watch during development:
flutter pub run build_runner watch

This creates injection.config.dart in the same folder. Never edit this file manually.

Core Annotations

AnnotationDescriptionget_it Equivalent
`@injectable`Registers as a factory (new instance each time)`registerFactory`
`@singleton`Registers as a singleton, created eagerly`registerSingleton`
`@lazySingleton`Registers as a singleton, created on first use`registerLazySingleton`
`@factoryParam`Marks a parameter that will be passed at creation time`registerFactoryParam`
`@preResolve`For async singletons, ensures initialisation completes before registration`registerSingletonAsync`

Basic Usage with Bloc

DARTRead-only
1
// lib/features/auth/data/datasources/auth_remote_datasource.dart
import 'package:injectable/injectable.dart';

@lazySingleton
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
  final HttpClient client;
  AuthRemoteDataSourceImpl(this.client);
}

// lib/features/auth/data/repositories/auth_repository_impl.dart
@injectable
class AuthRepositoryImpl implements AuthRepository {
  final AuthRemoteDataSource remoteDataSource;
  AuthRepositoryImpl(this.remoteDataSource);
}

// lib/features/auth/domain/usecases/login_usecase.dart
@injectable
class LoginUseCase {
  final AuthRepository repository;
  LoginUseCase(this.repository);
}
DARTRead-only
1
// lib/features/auth/bloc/auth_bloc.dart
import 'package:injectable/injectable.dart';

@injectable
class AuthBloc extends Bloc<AuthEvent, AuthState> {
  final LoginUseCase loginUseCase;
  final LogoutUseCase logoutUseCase;

  AuthBloc(this.loginUseCase, this.logoutUseCase) : super(AuthInitial()) {
    on<LoginRequested>(_onLoginRequested);
    on<LogoutRequested>(_onLogoutRequested);
  }
}
DARTRead-only
1
// lib/main.dart
import 'di/injection.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await configureDependencies(); // sets up getIt
  runApp(MyApp());
}

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

Advanced Patterns

When a dependency needs parameters that are only known at runtime, use @factoryParam.

DARTRead-only
1
@injectable
class UserBloc extends Bloc<UserEvent, UserState> {
  final UserRepository repository;
  final String userId;

  UserBloc(this.repository, @factoryParam this.userId) : super(...);
}

// Usage:
final userBloc = getIt<UserBloc>(param1: 'user-123');

If you have multiple implementations of the same interface, use @Named.

DARTRead-only
1
@singleton
@Named('api')
class ApiHttpClient implements HttpClient { ... }

@singleton
@Named('mock')
class MockHttpClient implements HttpClient { ... }

// Inject by name
@injectable
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
  AuthRemoteDataSourceImpl(@Named('api') this.client);
  final HttpClient client;
}

Use @Environment to register different implementations based on the build environment (e.g., dev, prod).

DARTRead-only
1
// lib/di/environments.dart
const dev = Environment('dev');
const prod = Environment('prod');

// Register only in dev environment
@singleton(env: [dev])
class DevHttpClient implements HttpClient { ... }

@singleton(env: [prod])
class ProdHttpClient implements HttpClient { ... }

// In configureDependencies, pass the environment:
await configureDependencies(environment: dev);

For dependencies that require async initialisation (like SharedPreferences), use @preResolve with @singleton.

DARTRead-only
1
@singleton
@preResolve
Future<SharedPreferences> get prefs async => await SharedPreferences.getInstance();

// Or with a custom class:
@singleton
class DatabaseHelper {
  DatabaseHelper._();
  static Future<DatabaseHelper> create() async { ... }

  @factoryMethod
  static Future<DatabaseHelper> _create() => DatabaseHelper.create();
}

Testing with Injectable

Testing becomes simpler because you can easily replace dependencies with mocks. In your test file, reset getIt and register mocks before each test.

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();
    getIt.reset();
    getIt.registerSingleton<AuthRepository>(mockRepo);
    // Also register other dependencies (or use injectable's test environment)
  });

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

    final useCase = LoginUseCase(mockRepo);
    final result = await useCase.execute('test@example.com', 'pass');
    expect(result.isRight(), true);
  });
}

Best Practices

  • Use @lazySingleton for most services – They are created only when needed and reused throughout the app.
  • Use @injectable for blocs – Blocs are often created per screen; factories ensure a new instance each time.
  • Group related registrations – Keep your DI code near the classes they register; injectable works across the whole project.
  • Use named bindings for multiple implementations – Makes the intention clear and avoids conflicts.
  • Leverage environments – Use @Environment to separate dev and prod configurations, especially for API clients and mock data.
  • Avoid circular dependencies – The generator will complain, but you can resolve by using @injectable(as: ...) or redesigning.
  • Commit generated files – It's safe to commit injection.config.dart to version control; it ensures consistent setup for all developers.

Common Mistakes

  • ❌ Forgetting to run build_runner – Changes to annotations won't be reflected until regeneration.
  • ❌ Using @singleton for stateful objects – State can persist across screens unexpectedly.
  • ❌ Not resetting getIt in tests – Causes test pollution; always call getIt.reset() in setUp or tearDown.
  • ❌ Over‑using @factoryParam – It's powerful but can make dependencies hard to trace. Prefer passing parameters via bloc events when possible.
  • ❌ Ignoring circular dependencies – Injectable will fail to generate; restructure your code to avoid circular references.

What's Next?

Now that you have a robust DI setup, you can build a modular, testable application. Next, explore how to structure your app with modular architecture and clean layers.

Next, explore Modular architecture with Bloc and Repository pattern.

Test Your Knowledge

Q1
of 3

Which annotation should you use to register a class as a singleton that is created on first use?

A
@singleton
B
@lazySingleton
C
@injectable
D
@factoryParam
Q2
of 3

What command generates the DI code for injectable?

A
flutter pub get
B
flutter pub run build_runner build
C
flutter gen
D
dart run injectable
Q3
of 3

How do you handle different implementations for dev and prod environments?

A
@Named
B
@Environment
C
@Profile
D
@Conditional

Frequently Asked Questions

Do I need to manually write get_it registration code when using injectable?

No. The generator writes it for you. You only need to annotate your classes and run the generator. The generated injection.config.dart contains all the registration code.

Can I use injectable with BlocProvider?

Yes. You can register blocs as injectable (usually as @injectable for factories) and then use BlocProvider(create: (_) => getIt<MyBloc>()) to provide them.

How do I handle async initialisation like SharedPreferences?

Use @singleton with @preResolve on a method that returns Future<T>. Injectable will await it before completing the registration, ensuring the dependency is ready.

Can I combine injectable with manual registrations?

Yes. The generated code runs after the manual registrations if you call getIt.init() after your manual setup. You can also exclude certain classes from generation using @injectable(as: ...).

Previous

bloc getit

Next

bloc api integration

Related Content

Need help?

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