What's the context of this Context?
@rallat Context!
— Gert Lombard (@codeblast) August 26, 2015
When I see Android code like this, it makes me feel uncomfortable:
class Foo {
private final Context mContext;
public Foo(Context context) {
mContext = context;
}
....
}
You may be thinking: “What on earth are you talking about? That’s a super common pattern in Android development to pass Context
around from class to class. We need a Context
instance for all sorts of things like accessing system services etc! Oh wait, I get it, you’re talking about the m
prefix for class member variable names which is specific to Java code in Android!”
No, I’m not talking about the weird and unnecessary “Hungarian notation” prefix in mContext
, although I agree it is an eyesore, but that’s a topic for a different blog post! ;-)
tl;dr: Inject/pass explicit types (e.g. LayoutInflater
) to classes where they are needed, instead of passing Context
all over the place.
I find the pervasiveness of this habit of passing a Context
instance into class constructors and method parameters confusing and disturbing. It’s unfortunate that the Android framework provided the Context
base class as a kind of god object or together with things like getSystemService()
as a kind of service locator - i.e. it’s the class that knows and does everything!
Why is it a bad idea to pass Context
to a class’s contructor or a method, when everyone does it and you can’t do anything in Android without a Context
?
Some examples of issues:
- If I’m not the original author of this code, I will wonder: which context is this? Activity, Application, Service?
- Can I hold a reference to this context in case I need it later? No, not unless it’s the application context. But how do I know for sure this is the application context? Especially if this class is used in different context further down the object graph and I don’t know who the caller is. It will eventually cause leaks somewhere e.g. activity leak, because some class will inadvertently hold a reference to the Activity.
- Can I use LayoutInflator on it? No, not if it’s not the context of the activity I’m expecting.
- It’s a violation of the Law of Demeter. For example, when I write the unit test for class
Foo
which takesContext
as a constructor dependency, how do I know which parts ofContext
I need to mock for the test to legitimately pass? If the class is sufficiently complex, I’ll just have to “guess” and figure it out by trial and error. Worse, I may have to mockA
to return a mock ofB
to return a mock ofC
… Now the unit test is becoming unwieldly.
A solution
Assuming you’re convinced at this point that it’s a bad idea to pass Context
down several levels from class to class, what can we do to improve the situation?
Apply this simple principle: communicate your intention explicitly in your code.
In other words, in many cases, you don’t really want the Context
reference itself, you just want to use it to invoke some action or retrieve some system service. In that case, rather pass the exact service(s) explicitly to the constructor of the class instead of Context
.
As a simple example to demonstrate the point, consider this snippet from a simple hypothetical list adapter:
private final Context mContext;
private final String[] mValues;
public MyListAdapter(Context context, String[] values) {
this.mContext = context;
this.mValues = values;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
LayoutInflater inflater = LayoutInflater.from(mContext);
View rowView = inflater.inflate(R.layout.rowlayout, parent, false);
...
We pass the Context
into the adapter, but the code doesn’t actually need a Context
, it really needs a LayoutInflater
instead! There is no need carrying the Context
around all over the place just in case we need it some day.
Let’s refactor this code to make it slightly easier to understand, reason about and slightly easier to unit test:
private final LayoutInflater mLayoutInflater;
private final String[] mValues;
public MyListAdapter(LayoutInflater layoutInflater, String[] values) {
this.mLayoutInflater = layoutInflater;
this.mValues = values;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View rowView = mLayoutInflater.inflate(R.layout.rowlayout, parent, false);
...
Even if you need need to retrieve multiple objects from Context
and not just one as in this example, then it’s still better to pass separate references to those objects.
Of course there are many exceptions to this advice, because often you’ll be forced to pass Context
around through several layers of an object hierarchy because some leaf node which you didn’t write requires it, but even then, you can still be more explicit with your paramter types (and/or parameter names) to communicate your intention more clearly.
For examle, let’s say our list adapter really needs the activity Context
, then I’d propose refactoring the code to accept an Activity
instance instead, so it’s clear what kind of Context
we’re expecting here:
public MyListAdapter(Activity activityContext, String[] values) {
this.mContext = activityContext;
this.mValues = values;
}
Or if you’re using Dagger for dependency injection, you could use qualifier annotations like @ActivityContext
and @ApplicationContext
to differentiate (or even better if possible, separate your Dagger scopes to make it impossible to inject an Activity reference into a service that will outlive the activity…)
Also see: Context, What Context? by Dave Smith