Dependency Injection (Dagger 2)

The previous lesson split RecipeBox into Presentation, Domain, and Data layers — but something still has to actually build a RecipeRepositoryImpl, hand it a RecipeDao and a RecipeApiService, wrap it in a SaveRecipeUseCase, and hand that to a ViewModel. Dagger 2 is a code-generation library that does this wiring automatically.

The Problem: Manual Wiring

Without DI, every Activity that needs a RecipeRepository ends up constructing the entire dependency chain by hand:

// Every Activity repeats this — and repeats it slightly differently over time
RecipeDao dao = AppDatabase.getInstance(this).recipeDao();
RecipeApiService api = RetrofitClient.getInstance().create(RecipeApiService.class);
RecipeRepository repository = new RecipeRepositoryImpl(dao, api);
SaveRecipeUseCase saveRecipeUseCase = new SaveRecipeUseCase(repository);
Explanation: This works, but it duplicates setup logic across every screen, makes swapping a real repository for a fake one in tests painful, and silently breaks the dependency-direction rule from the previous lesson — the Activity now has to know about Room and Retrofit just to construct things.

@Module and @Provides

A @Module is a class that tells Dagger how to build objects it can't construct with a plain constructor — like a Retrofit service or a Room database.

// di/AppModule.java
@Module
public class AppModule {

    @Provides
    @Singleton
    RecipeDao provideRecipeDao(Application app) {
        return AppDatabase.getInstance(app).recipeDao();
    }

    @Provides
    @Singleton
    RecipeApiService provideRecipeApiService() {
        return RetrofitClient.getInstance().create(RecipeApiService.class);
    }

    @Provides
    @Singleton
    RecipeRepository provideRecipeRepository(RecipeDao dao, RecipeApiService api) {
        return new RecipeRepositoryImpl(dao, api);
    }
}
Explanation: Each @Provides method's parameters are themselves dependencies Dagger will resolve — notice provideRecipeRepository asks for a RecipeDao and RecipeApiService without caring how they were built.

@Inject Constructor Injection

Classes RecipeBox owns outright — like SaveRecipeUseCase — don't need a @Module at all. Annotating the constructor with @Inject is enough for Dagger to know how to build one.

// domain/SaveRecipeUseCase.java
public class SaveRecipeUseCase {

    private final RecipeRepository repository;

    @Inject
    public SaveRecipeUseCase(RecipeRepository repository) {
        this.repository = repository;
    }

    public void execute(Recipe recipe) {
        repository.save(recipe);
    }
}

@Component: Wiring It Together

A @Component is the bridge between the modules and the Android classes that need the dependencies — it's what Dagger uses to generate the actual factory code at compile time.

// di/AppComponent.java
@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {
    void inject(AddRecipeActivity activity);
}

Field Injection Into an Activity

// AddRecipeActivity.java
public class AddRecipeActivity extends AppCompatActivity {

    @Inject
    SaveRecipeUseCase saveRecipeUseCase;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ((RecipeBoxApp) getApplication()).getAppComponent().inject(this);
        setContentView(R.layout.activity_add_recipe);

        // saveRecipeUseCase is already built and ready to use here
    }
}
Explanation:
  • Activities can't use constructor injection (the OS creates them), so Dagger falls back to field injection — annotate the field, then call inject(this).
  • getAppComponent() typically lives on a custom Application subclass, built once when the app starts.

@Singleton Scope

Marking a @Provides method (or the whole @Component) with @Singleton tells Dagger to build that object once and hand back the same instance every time it's requested — exactly what's wanted for a shared RecipeRepository or database instance, instead of a fresh one per screen.

Manual DI vs. Dagger

Manual wiringDagger 2
BoilerplateRepeated in every ActivityGenerated once, reused everywhere
Swapping for testsHard — real objects are hardcodedEasy — provide a test module
Errors caughtAt runtime (crash)At compile time (build fails)
Learning curveNoneSteep — annotations, generated code, unfamiliar errors
Honest note: Dagger's compile-time errors and generated code make it genuinely difficult to learn at first — cryptic error messages are a common complaint. If the Kotlin track is available, its Hilt lessons cover a much simpler API built directly on top of Dagger, purpose-built for Android and hiding most of this ceremony. Dagger itself remains valuable to understand because Hilt's simplicity is built from these exact same concepts underneath.

Hands-On Practice

  1. Add Dagger 2 to a RecipeBox module and create an AppModule providing RecipeDao and RecipeApiService.
  2. Annotate SaveRecipeUseCase's constructor with @Inject.
  3. Field-inject it into an Activity and confirm the app builds and runs unchanged.
Dagger solves wiring for classes inside one module. The next lesson, Modularization, covers splitting RecipeBox itself into separate Gradle modules as it grows.