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.