Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Advanced Dagger 2 Patterns: Component Dependencies, Subcomponents, and Managed Instances

Tech May 28 1

Component Dependencies via the dependencies Attribute

In modular architectures, isolated dependency graphs frequently need to share specific bindings without duplicating module configurations. Dagger solves this by allowing one component to explicitly depend on another through the dependencies attribute of the @Component annotation. This approach establishes a read-only relationship where the dependent component can access exposed bindings from the dependency component.

To implement this, the dependency component must expose abstract methods that return the exact types the dependent component requires. Consider a scenario where an authentication module supplies a token that a storage module needs to initialize network requests.

@Module
public class AuthModule {
    @Provides
    @Named("BearerToken")
    public String provideAuthKey() {
        return "secure-token-" + System.currentTimeMillis();
    }
}

@Component(modules = AuthModule.class)
public interface AuthComponent {
    @Named("BearerToken") String getToken();
}

@Module
public class StorageModule {
    @Provides
    public LocalDatabase provideDatabase() {
        return new LocalDatabase();
    }
}

@Component(
    modules = StorageModule.class,
    dependencies = AuthComponent.class
)
public interface StorageComponent {
    void inject(SyncService service);
}

When constructing the component graph, Dagger's generated builder automatically accepts the dependency component as a parameter. The flow looks like this:

AuthComponent authGraph = DaggerAuthComponent.builder()
    .authModule(new AuthModule())
    .build();

StorageComponent storageGraph = DaggerStorageComponent.builder()
    .storageModule(new StorageModule())
    .authComponent(authGraph)
    .build();

SyncService sync = new SyncService();
storageGraph.inject(sync);

The SyncService can now safely request the authenticated token or database instances. This patern is particularly useful for sharing singleton infrastructure objects (like network clients or configuration managers) across multiple feature-specific components without creating tight coupling or redefining modules.

Hierarchical Graphs with @Subcomponent

When a dependency graph grows in complexity or requires a distinct lifecycle, splitting it into subcomponents is the recommended approach. A @Subcomponent automatically inherits all bindings from its parent component, eliminating the need to redeclare modules or scopes. Subcomponents are instantiated through the parent, ensuring that their lifecycle is properly tied to the parent's scope.

Continuing with the architecture example, assume the application requires a user-specific session graph that manages UI state and temporary cache. This session should only exist while a user is logged in.

@Subcomponent
public interface UserSessionComponent {
    void inject(HomeActivity activity);
    UserPreferences getUserPrefs();
}

@Singleton
@Component(modules = StorageModule.class, dependencies = AuthComponent.class)
public interface AppGraph {
    UserSessionComponent createUserSession();
}

The parent component exposes a method that returns the subcomponent interface. Dagger generates a builder for the subcomponent, allowing additional child-specific modules to be passed during creation. Usage follows a clear hierarchy:

AppGraph appGraph = DaggerAppGraph.builder()
    .storageModule(new StorageModule())
    .authComponent(authGraph)
    .build();

UserSessionComponent session = appGraph.createUserSession();
HomeActivity activity = new HomeActivity();
session.inject(activity);

Subcomponents inherit the parent's scopes and dependencies. If the parent is scoped with @Singleton, the subcomponent can define its own narrower scope (e.g., @UserScope) without conflict. This mechanism enforces strict memory management, as subcomponents can be explicitly discarded when no longer needed, releasing references held within their graph.

Instance Management with Lazy and Provider

Dagger provides two standard wrapper types from the javax.inject and dagger packages to control when and how often dependencies are instantiated. Understanding the distinction between Lazy<T> and Provider<T> is critical for optimizing performance and managing object lifecycles.

Lazy<T> defers the instantiation of a dependency until the get() method is explicitly called. Once resolved, the instance is cached within the lazy wrapper. Subsequent calls to get() return the same cached reference, respecting the component's scoping rules. This is ideal for heavy objects that may not be needed immediately upon component creation.

Provider<T>, on the other hand, acts as a factory. Every invocation of get() triggers a fresh resolution through the component graph. If the binding is unscoped, a new instance is created each time. If the binding carries a scope, the scoped instance is returned, but the resolution logic still executes. This is essential when you need multiple distinct instances of a type or when you want to manually control the timing of dependency resolution.

Implementing both patterns in a service class demonstrates their behavioral difference:

public class DataProcessor {

    @Inject
    @Named("PrimaryDB")
    Lazy<databaseconnection> dbConnection;

    @Inject
    Provider<reportgenerator> reportFactory;

    public void processRecords() {
        // First call initializes and caches the connection
        dbConnection.get().open();
        
        // Subsequent calls reuse the cached instance
        dbConnection.get().executeQuery("SELECT * FROM logs");

        // Each call triggers a new instance creation
        ReportGenerator r1 = reportFactory.get();
        ReportGenerator r2 = reportFactory.get();
        
        System.out.println("Same DB instance: " + (dbConnection.get() == dbConnection.get()));
        System.out.println("Same Report instance: " + (r1 == r2));
    }
}
</reportgenerator></databaseconnection>

The output confirms the caching behavior of Lazy versus the fresh instantiation pattern of Provider. Using these wrappers avoids premature initialization, reduces startup overhead, and provides explicit control over object creation cycles within the dependency graph.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.