In our previous article, we delved deep into the world of subcomponents and custom scopes in Dagger. Today, we're going to elevate our Dagger game by exploring two pivotal concepts: Subcomponent Builders and Component Factories.
Subcomponent Builders
Subcomponent Builders, akin to component builders, allow us to employ the builder pattern for our sub-components. This facilitates the seamless passage of dynamic values into our Dagger graph.
Let's directly dive into our example code and see what we need to change.
@PerActivity
@Subcomponent
public interface ActivityComponent {
ComputeLayer getComputeLayer();
void inject (MainActivity mainActivity);
@Subcomponent.Builder
interface Builder{
ActivityComponent build();
}
}
Here, we've introduced the @Subcomponent.Builder
annotation and incorporated a custom builder within our ActivityComponent
. The subsequent step involves updating our ComputeComponent
and MainActivity
classes:
@Singleton
@Component (modules = NetworkModuleSecond.class)
public interface ComputeComponent {
ActivityComponent.Builder getActivityBuilder(); // ActivityComponent.Builder used instead of ActivityComponent
@Component.Builder
interface Builder{
ComputeComponent build();
@BindsInstance
Builder delay(@Named("delay") int delay);
@BindsInstance
Builder status(@Named("status")int status);
Builder networkModuleSecond(NetworkModuleSecond networkModuleSecond);
}
}
Remember that if we are using a subcomponent builder, we need to return the builder and not the subcomponent itself, as shown above.
class MainActivity : AppCompatActivity() {
var calculate : Button? = null
var TAG = this.javaClass.canonicalName
@Inject
lateinit var computation : ComputeLayer
@Inject
lateinit var computation2 : ComputeLayer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
calculate = findViewById(R.id.calculate_sum)
val component = (application as MyApp).appComponent
.activityBuilder
.build()
component.inject(this)
}
}
As expected, we have changed the component here to the builder.build()
method as used earlier for components.
And that's it, now you can add your custom builder methods into the subcomponent like we did earlier for the ComputeComponent
.
Component & Subcomponent Factory
While the Component.Builder
interface is handy, it lacks compile-time safety. This means there's a risk of overlooking a builder method, and unfortunately, Android Studio won't flag such oversights.
Enter Component.Factory
, which offers the much-needed compile-time safety. Let's see how it works:
For this, we'll update our classes as follows.
@Singleton
@Component (modules = NetworkModuleSecond.class)
public interface ComputeComponent {
ActivityComponent.Builder getActivityBuilder();
@Component.Factory
interface Factory{
ComputeComponent create(@BindsInstance @Named("delay") int delay
, @BindsInstance @Named("status")int status,
NetworkModuleSecond networkModuleSecond);
}
}
The standout feature here is the consolidation of multiple methods into a singular method, accepting all necessary objects and values as arguments.
public class MyApp extends Application {
private ComputeComponent component;
@Override
public void onCreate() {
super.onCreate();
component = DaggerComputeComponent
.factory()
.create(100, 10, new NetworkModuleSecond());
}
public ComputeComponent getAppComponent(){
return component;
}
}
Notice that here we are passing the values for delay
, status
and the NetworkModuleSecond
not using builder methods but as params to a single factory method. This creates a compile-time safety layer because if we forget to add one of the three parameters, the project will not compile at all.
And that's it, if you run the project, you'll get the same result as earlier!
Conclusion
Characteristics of Component.Builder:
Flexibility: It allows for step-by-step construction of the component. This is especially useful when you have multiple modules with different configurations.
Verbose: Requires more boilerplate code. For each module or dependency that needs to be set at runtime, you'll need a separate method in the builder.
Error-prone: If you forget to provide a required module or dependency, the error might not be caught until runtime.
Characteristics of Component.Factory:
Conciseness: Requires less boilerplate code compared to
Builder
. All required dependencies can be provided in a single method.Safety: If a required module or dependency is missing, you'll get a compile-time error, making it less error-prone than
Builder
.Simplicity: It's straightforward and doesn't require multiple methods for different modules or dependencies.
How to Choose?
Simplicity & Safety: If you value simplicity and want to catch errors at compile-time, go with
Component.Factory
. It's especially useful when all of your modules have constructor arguments.Flexibility: If you need more control over the component's creation and have multiple modules with different configurations,
Component.Builder
might be a better fit. However, be cautious about runtime errors.Migration: If you're refactoring or migrating from an older version of Dagger, and your codebase already uses
Component.Builder
, it might be easier to stick with it unless you have strong reasons to switch toFactory
.