Custom Scopes & Subcomponents in DI: (Day 11)

So far, we've experimented with the @Singleton scope, understanding its ability to create, retain, and reuse objects. However, there are scenarios that @Singleton can't address. For instance, what if we want an object to be freshly instantiated with every new activity creation, yet consistently reused across all its child fragments? The @Singleton annotation isn't tailored for such specific requirements.

Enter custom scopes. These powerful tools allow us to define scopes beyond what's provided by AndroidX, granting us the flexibility to manage object lifecycles as needed.

To understand this concept deeply, let's build on top of our last example.

Let's suppose that we'd like our NetworkLayer to be created only once in our application scope, but the ComputeLayer should be recreated for each activity.

So, first of all, you might have already guessed that to create the NetworkLayer only once in the application scope, there's not much that we need to do. As long as it has a singleton scope application it should be recreated only once.

So, let's try to figure out how can we recreate our custom scope for ComputeLayer.

Step 1

For this, our first step would be to define a new scope as follows.

@Scope
@Documented
@Retention(RUNTIME)
public @interface PerActivity {
}

Now, we can use @PerActivity as an annotation in our application.

Step 2

Now, let's go ahead and annotate our ComputeLayer with this annotation as follows

@PerActivity
public class ComputeLayer {
    String TAG = this.getClass().getCanonicalName();
    String layer = "Computation";
    NetworkLayer network;

    @Inject
    public ComputeLayer(NetworkLayer networkLayer){
        this.network = networkLayer;
        Log.e(TAG, "Compute Layer Created uses network layer as " + networkLayer);
    }
//...
}

Step 3

Now, we need to create a new @Subcomponent that will have the scope for an Activity as follows.

@PerActivity
@Subcomponent
public interface ActivityComponent {
    ComputeLayer getComputeLayer();

    void inject  (MainActivity mainActivity);
}

Notice, that instead of annotating this component with @Component we have annotated it with @Subcomponent, because this gives us the capability to access its parent component's dependency graph which is ComputeComponent in our case.

This is important because ComputeComponent has the NetworkModuleSecond object which will later be needed by the ComputeLayer class.

Step 4

Now, all we need to do is to tell our ComputeComponent about the ActivityComponent we just created like this

@Singleton
@Component (modules = NetworkModuleSecond.class)
public interface ComputeComponent {

    ActivityComponent getActivityComponent();
    @Component.Builder
    interface Builder{
        ComputeComponent build();

        @BindsInstance
        Builder delay(@Named("delay") int delay);

        @BindsInstance
        Builder status(@Named("status")int status);

        Builder networkModuleSecond(NetworkModuleSecond networkModuleSecond);
    }
}

Step 5

Now, we'll go into our MainActivity class, and create our ActivityComponent there

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)

        //Code changed here
        val component = (application as MyApp).appComponent.activityComponent
        component.inject(this)

    Log.e(TAG, "computation 1 is : $computation + and second computation is $computation2" )

        calculate?.setOnClickListener {
            computation.add(1,1)
            Log.e(TAG, "Button click uses computeLayer to be ")
        }
    Log.e(TAG, "Main Activity Created")
    }
}

And that's all! If we run our app now and compare the hashes of our objects created, you will notice that the NetworkLayer object was created only once even if we rotate the screen, but the ComputeLayer gets created every time we rotate our device orientation.

Results

Before Orientation Changed:
ComputeLayer : computation 1 is : com.example.understandingdependencyinjection.ComputeLayer@210414 + and second computation is com.example.understandingdependencyinjection.ComputeLayer@210414
NetworkLayer : Compute Layer Created uses network layer as com.example.understandingdependencyinjection.NetworkSetupSecond@2b09e67

After Orientation Changed:
ComputeLayer : computation 1 is : com.example.understandingdependencyinjection.ComputeLayer@98d0745 + and second computation is com.example.understandingdependencyinjection.ComputeLayer@98d0745
NetworkLayer : Compute Layer Created uses network layer as com.example.understandingdependencyinjection.NetworkSetupSecond@2b09e67

As evident from the logs above, you can see that the NetworkLayer object wasn't renewed even after we rotated the device but the ComputeLayer did, which was annotated with @PerActivity

Remember!

Remember, no subcomponent can have the same scope as any ancestor component. However, two unrelated subcomponents can share the same scope, ensuring there's no ambiguity in storing scoped objects.

Conclusion

Custom scopes and subcomponents in Dagger2 offer a flexible way to manage object lifecycles in Android applications. By understanding and implementing these concepts, developers can optimize object creation and reuse, leading to more efficient and maintainable code.