System Services are not just for the system
I came across an interesting behavior when using Dagger and Custom Views. In most cases, my apps have 2 components. There is a top level AppComponent for Singletons and an Activity Component for anything that should be “one per activity”. Within my activities there is the normal dagger boilerplate:
activityComponent = application.getAppComponent()
.plusActivityComponent(new ActivityModule(this));
Additionally I like to expose the activity component in a getter so that views can have access to the activity scoped objects:
public interface ActivityComponentProvider {
Activitycomponent getActivityComponent();
}
within views I can then do:
public VrView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(getContext(), R.layout.vr_view_contents, this);
((ActivityComponentProvider) context).getActivityComponent().inject(this);
In most cases this works but yesterday was not most case. The story started with me creating an xml layout that contains a <android.support.design.widget.AppBarLayout> tag. Inside of that I put a custom view <com.sample.myview>. Similar to the above example I added the injection code to the views constructor and ran my code and Crash. To my surprise I started getting “java.lang.ClassCastException: android.view.ContextThemeWrapper cannot be cast to android.app.Activity”
errors. After a bit of digging, I was able to deduce that the problem is due to how AppCompat inflates views. Unlike regular widgets/layout groups, <AppBarLayout> children use a ContextWrapper to inflate child views rather than the containing Activity’s context.
The brute force solution was to call getBaseContext() on the view’s context over and over again until you get to the activity but this seemed hacky and strange. Instead I was pointed to an interesting solution on how to pass a Dagger Component from an activity to a view (Thanks Jake Wharton!)
The novel approach was to leverage an activity’s getSystemService override. Let’s go through a example.
The first thing you’ll want to do is override getSystemService in your activity.
@Override
public Object getSystemService(String name) {
return super.getSystemService(name);
}
The context.getSystemService call normally is used to get a reference to things like the LayoutInflator. By overriding the method in your activity, you’ll now be able to return an instance of anything you want for a particular key. In our case we’d like to return the Activity component
@Override
public Object getSystemService(String name) {
if("Dagger".equals(name)){
return activityComponent;
}
return super.getSystemService(name);
}
Notice that we still want to call through to super if it is not the key that we are interested it.
Now within our view’s constructor we can access the activity component by calling getContext().getSystemService(key). The nice thing about this setup is that you don’t have to unwrap the context to find the activity, the system service call will do that for you.
public VrView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(getContext(), R.layout.vr_view_contents, this);
ActivityComponent activityComponent = (ActivityComponent) getContext().getSystemService("Dagger");
activitycomponent.inject(this);
Rather than making the dangerous cast from getContext to your activity, we can leverage androids system service architecture.
Finally to make Android Studio lint checker happy, add
//noinspection ResourceType
above
ActivityComponent activityComponent = (ActivityComponent) getContext().getSystemService("Dagger");
And thats it! You can now get a reference to your activity component from a view. Bonus: In a similar manner you can override getSystemService in your Application class and pass the app component from there. This would allow you to do something like
AppComponent component = (AppComponent) getAppplicationContext().getSystemService("AppComponent");
from your activities.
Thanks again to those that helped me grok this yesterday.