Component Builders & Factory: (Day 12)

Photo by Igor bispo on Unsplash

Component Builders & Factory: (Day 12)

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:

  1. Flexibility: It allows for step-by-step construction of the component. This is especially useful when you have multiple modules with different configurations.

  2. 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.

  3. Error-prone: If you forget to provide a required module or dependency, the error might not be caught until runtime.

Characteristics of Component.Factory:

  1. Conciseness: Requires less boilerplate code compared to Builder. All required dependencies can be provided in a single method.

  2. Safety: If a required module or dependency is missing, you'll get a compile-time error, making it less error-prone than Builder.

  3. Simplicity: It's straightforward and doesn't require multiple methods for different modules or dependencies.

How to Choose?

  1. 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.

  2. 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.

  3. 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 to Factory.