← Back to Home

Module 3: Dependency Injection 2

Module Overview

Advanced dependency injection concepts and Dagger implementation. Build on your DI knowledge with more complex patterns and frameworks.

Learning Objectives

Video Content: Dagger @Binds Annotation

This video explains the @Binds annotation in Dagger, which is a more efficient way to bind an interface to its implementation compared to @Provides methods.

// Interface definition
public interface UserRepository {
    User findById(String id);
    void save(User user);
}

// Implementation
public class SQLUserRepository implements UserRepository {
    private final Database database;
    
    @Inject
    public SQLUserRepository(Database database) {
        this.database = database;
    }
    
    @Override
    public User findById(String id) {
        // Implementation using database
        return database.executeQuery("SELECT * FROM users WHERE id = ?", id);
    }
    
    @Override
    public void save(User user) {
        // Implementation using database
        database.executeUpdate("INSERT INTO users VALUES (?, ?, ?)",
            user.getId(), user.getName(), user.getEmail());
    }
}

// Module with @Binds - more efficient than @Provides
@Module
public abstract class RepositoryModule {
    // Bind UserRepository interface to SQLUserRepository implementation
    @Binds
    abstract UserRepository bindUserRepository(SQLUserRepository impl);
}

This example demonstrates the use of @Binds in a Dagger module to efficiently bind an interface (UserRepository) to its implementation (SQLUserRepository). The @Binds method is abstract, as Dagger only needs to know about the binding relationship, not how to construct the implementation.

Video Content: DI Component Creation

This video covers how to create and use Dagger components in different scenarios, including component factories and instance binding in component builders.

// Component with a Builder pattern
@Singleton
@Component(modules = {AppModule.class, NetworkModule.class})
public interface AppComponent {
    UserService userService();
    
    // Component.Builder pattern with runtime values
    @Component.Builder
    interface Builder {
        // Bind a module that requires parameters
        @BindsInstance
        Builder apiKey(@Named("api-key") String apiKey);
        
        // Set a module instance
        Builder networkModule(NetworkModule module);
        
        // Build method returns the component
        AppComponent build();
    }
}

// Using the component builder
public class MyApplication {
    private AppComponent component;
    
    public void onCreate() {
        // Create network module with custom configuration
        NetworkModule networkModule = new NetworkModule("https://api.example.com");
        
        // Build component with runtime values
        component = DaggerAppComponent.builder()
            .apiKey("my-secret-key")
            .networkModule(networkModule)
            .build();
    }
}

This example shows how to use Dagger's Component.Builder pattern to create a component with runtime values. The @BindsInstance annotation allows binding values directly into the graph, while modules can also be provided as instances when they require custom configuration.

Video Content: Subcomponents and Scopes

This video explores Dagger subcomponents and custom scopes, which help organize dependencies based on their lifecycles in complex applications.

// Custom scope annotation for user session
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface SessionScope {}

// Parent component (application-wide singleton scope)
@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {
    // Factory method to create the session subcomponent
    SessionComponent.Factory sessionComponentFactory();
}

// Subcomponent with session scope
@SessionScope
@Subcomponent(modules = {SessionModule.class})
public interface SessionComponent {
    // Dependencies provided by this subcomponent
    UserManager userManager();
    ShoppingCart shoppingCart();
    
    // Factory to create this subcomponent
    @Subcomponent.Factory
    interface Factory {
        SessionComponent create(@BindsInstance User user);
    }
}

// Usage example
public class MyApp {
    private AppComponent appComponent;
    private SessionComponent sessionComponent;
    
    public void onCreate() {
        appComponent = DaggerAppComponent.create();
    }
    
    public void onUserLogin(User user) {
        // Create session-scoped component when user logs in
        sessionComponent = appComponent.sessionComponentFactory().create(user);
        
        // Get session-scoped dependencies
        UserManager userManager = sessionComponent.userManager();
        ShoppingCart cart = sessionComponent.shoppingCart();
    }
    
    public void onUserLogout() {
        // Clear session component on logout
        sessionComponent = null;
    }
}

This example demonstrates using subcomponents with custom scopes to manage different dependency lifecycles. The @SessionScope indicates dependencies that should live for the duration of a user session. The parent @Singleton component provides application-wide dependencies, while the @SessionScope subcomponent manages session-specific dependencies.

Video Content: Sprint 14 Dependency Injection 2 Overview

This video brings together the advanced dependency injection concepts covered in this module, presenting a comprehensive view of advanced Dagger usage.

// Complete advanced DI example
// Multibinding with a map - useful for plugins or feature modules
@Module
public abstract class ProcessorsModule {
    // Bind multiple implementations to a map
    @Binds
    @IntoMap
    @StringKey("json")
    abstract DataProcessor bindJsonProcessor(JsonProcessor impl);
    
    @Binds
    @IntoMap
    @StringKey("xml")
    abstract DataProcessor bindXmlProcessor(XmlProcessor impl);
    
    @Binds
    @IntoMap
    @StringKey("csv")
    abstract DataProcessor bindCsvProcessor(CsvProcessor impl);
}

// Service using map injection
public class DataProcessingService {
    private final Map<String, DataProcessor> processors;
    
    @Inject
    public DataProcessingService(Map<String, DataProcessor> processors) {
        this.processors = processors;
    }
    
    public void processData(String format, String data) {
        DataProcessor processor = processors.get(format);
        if (processor == null) {
            throw new IllegalArgumentException("No processor found for format: " + format);
        }
        processor.process(data);
    }
}

This advanced example shows Dagger's multibinding feature for injecting a map of processors indexed by a key. This pattern is powerful for plugin architectures where different implementations can be selected at runtime. The @IntoMap annotation with key annotations helps Dagger build the map automatically from all the bound implementations.

Resources