Skip to content

Dependency Injection Libraries

Dependency Injection libraries help build object graphs. They do not replace the underlying design rule which posits that ordinary code should declare dependencies and receive them from the outside.

Two useful reference points are Dagger and get_it. Dagger is a compile-time dependency graph tool for Java and Kotlin. get_it is a Dart service locator commonly used in Flutter.

Dagger builds a dependency graph at compile time using annotations.

Constructor-inject classes that Dagger can create directly.

final class UserRepository {
private final ApiClient apiClient;
@Inject
UserRepository(ApiClient apiClient) {
this.apiClient = apiClient;
}
}

Use modules when constructor injection is not enough for interfaces, third-party classes, and configured objects.

interface ApiClient {
fun get(path: String): String
}
class HttpApiClient(private val baseUrl: String) : ApiClient {
override fun get(path: String): String = "$baseUrl$path"
}
@Module
object NetworkModule {
@Provides
fun provideApiClient(): ApiClient = HttpApiClient("https://api.example.com")
}

A component is the graph boundary.

@Component(modules = [NetworkModule::class])
interface AppComponent {
fun userRepository(): UserRepository
}

Dagger’s main advantage is compile-time validation. Missing or invalid bindings fail the build instead of surfacing as late runtime lookup errors.

  • Annotation processing or KSP setup.
  • Generated code to understand during debugging.
  • More ceremony than pure DI for small apps.
  • Graph shape and scopes need care as the app grows.

get_it is a service locator for Dart and Flutter. It stores factories and instances behind a global or passed locator.

final getIt = GetIt.instance;
void configureDependencies() {
getIt.registerLazySingleton<ApiClient>(
() => ApiClient(baseUrl: Uri.parse('https://api.example.com')),
);
getIt.registerFactory<ProfileBloc>(
() => ProfileBloc(getIt<ApiClient>()),
);
}
RegistrationLifetimeUse
registerSingletonCreated immediately, reusedCheap service needed at startup
registerLazySingletonCreated on first lookup, reusedShared service that may be expensive
registerFactoryNew instance on each lookupShort-lived object such as a bloc or controller

The practical risk is hidden dependency access.

class ProfileBloc {
ProfileBloc() : repository = GetIt.I<ProfileRepository>();
final ProfileRepository repository;
}

This works, but the constructor no longer tells the reader what ProfileBloc needs. A better pattern is to use get_it in the composition root and keep constructor injection inside features.

getIt.registerFactory<ProfileBloc>(
() => ProfileBloc(repository: getIt<ProfileRepository>()),
);

Use pure DI when the graph is small enough to wire by hand.

Use Dagger when a Java or Kotlin app has a large static graph and compile-time validation is worth the ceremony.

Use get_it when a Flutter app needs lightweight centralized wiring, but keep lookups near startup. Treat it as composition infrastructure, not as a dependency access pattern inside business objects.