Android Context Needs Isolation

I bet you know that Android framework is built around hierarchy of objects that extend Context abstract class.

What might come as a surprise is that the hierarchy of Context objects is one of the main causes for spaghetti code in Android projects. There are many issues associated with this hierarchy, but the worst of all is that its members have too many responsibilities and know too much about the system.

This extensive knowledge and multiplication of responsibilities constitute violation of so-called Single Responsibility Principle.

As I explained in the article about Single Responsibility Principle, classes that violate this principle are “contagious”. Other classes that “come into contact” with SRP violators classes are at risk of quickly becoming violators by themselves.

Fortunately, most of the pitfalls associated with Context objects can be worked around by following one simple rule. In this post I’m going to share this rule with you and explain how it can make your code better.

Context Rule:

In respect to Context objects I formulated one simple rule for myself which I call “Context Rule”: my own classes should avoid having references to Context objects.

The rational behind this rule is very simple – if my classes won’t have references to Context objects then they won’t become coupled to multiple Context responsibilities over time.

At this point this rule might seem absolutely unpractical because your intuition says that nothing in Android can be done without Context. At least that’s what my intuition said for a very long time. However, in this specific case, my intuition was wrong.

In the following sections I will show you how to follow Context Rule and minimize the coupling to Context objects in several common scenarios.

Context as proxy to other objects:

I used to inject Context into my classes even when they needed completely different objects. In this scenario, in addition to their actual dependencies, classes also used Context as a proxy to these dependencies.

Common examples of this practice involve retrieval of SharedPreferences and various system services:

public class MyClass {
    private final SharedPreferences mSharedPreferences;
    private final BatteryManager mBatteryManager;

    public MyClass(Context context) {
        mSharedPreferences = context.getSharedPreferences(SHARED_PREFS_FILE, Context.MODE_PRIVATE);
        mBatteryManager = (BatteryManager) context.getSystemService(BATTERY_SERVICE);;
    }
}

There are several issues with this implementation of MyClass.

First of all, the name of the class is really bad, but this is intentional – I want you to see the benefits of following the Context Rule.

Since Context objects have lots of responsibilities, the constructor signature of this class – MyClass(Context) – provides no hints as to what this class does.

In addition, the internal implementation will probably lead to lots of code duplication. For instance, it is very probable that if I follow this practice then the line which obtains a reference to SharedPreferences object will be duplicated in many places inside the codebase.

This approach of using an object just to get references to other objects constitutes violation of so-called Law of Demeter.

Law of Demeter violations might seem pretty innocent on the first sight, but they tend to accumulate and spread until the codebase becomes a big entangled mess.

Removing dependency on Context proxy:

Fortunately, it is very easy to fix the design and remove Context when it’s just used as a proxy.

All I needed to do is to remove the unnecessary proxy:

public class MyClass {
    private final SharedPreferences mSharedPreferences;
    private final BatteryManager mBatteryManager;

    public MyClass(SharedPreferences sharedPreferences, BatteryManager batteryManager) {
        mSharedPreferences = sharedPreferences;
        mBatteryManager = batteryManager;
    }
}

While the change wasn’t impressive and the resulting implementation is very similar to the original, it has huge advantages:

  1. I eliminated the need for code duplication by removing from MyClass the logic that retrieved the actual dependencies from Context proxy.
  2. The new constructor signature – MyClass(SharedPreferences, BatteryManager) – provides useful hints as to what MyClass might be responsible for.
  3. Unit testing of MyClass became easier because its constructor clearly states which classes need to be substituted with test-doubles.

Since Context objects in Android can function as proxies to many other objects, classes that depend on Context (and its sub-classes) are the most common violators of Law of Demeter. By following Context Rule you can greatly reduce the risk of such violations in your project.

Context as functionality provider:

In the previous section I demonstrated how to avoid dependency on Context objects when it is just used as proxy to other objects. However, there are lots of responsibilities which are fused into Context’s functional methods. Is there anything I can to to avoid dependency on Context in this case?

Well, sort of.

Let’s take external cache directory as an example. This directory is intended for storing non-sensitive information that needs to be deleted if the application is uninstalled.

A class that performs some manipulation on external cache directory might look like this:

public class MyClass {
    private final Context mContext;

    public MyClass(Context context) {
        mContext = context;
    }

    public void doSomething() {
        File externalCacheDir = mContext.getExternalCacheDir();
        if (externalCacheDir != null) {
            ...
        }
    }
}

That’s not bad compared to having all this functionality bundled inside Activity or Fragment, but there are still some issues with it.

Just like in the previous example, the constructor signature of this class – MyClass(Context) – says nothing about what it might be responsible for.

In addition, the code that retrieves the reference to external cache directory and ensures that it exists will be duplicated in each component that needs an access to this directory. Duplicating this code in two or three places might not look like a big issue, but it is.

For instance, in API 19 a method called getExternalCacheDirs() was introduced. It is similar to getExternalCacheDir(), but accounts for the possibility of having multiple cache directories.

If I want to make use of this new method while the code duplicated in several places, I need to duplicate the change as well. Since I would need to manually ensure that all instances of this (or similar) code are updated, I could easily miss one or more places, thus introducing inconsistency bugs. These bugs are very hard to trace and fix.

Another issue with this implementation is related to the fact that requirements constantly change. As new requirements emerge, MyClass might become coupled to more and more Context responsibilities. This additional coupling will multiply the aforementioned issues by the number of Context responsibilities used inside MyClass class.

If you unit test your classes then multiplicity of internal couplings to Context will require very complicated mocking strategy for Context. If you also happen to have unitTests.returnDefaultValues = true directive in build.gradle configuration (which should be avoided in general), then you can easily be writing flaky or even wrong tests.

Removing dependency on Context functionality provider:

The root cause of all the above issues is the fact that Context violates Single Responsibility Principle and MyClass was contaminated by having a direct reference to Context.

Think about this in the following way: whatever MyClass does, handling the specifics of interaction with external cache directory is not its intrinsic responsibility.

The fix for these issues is straightforward – we must extract the unrelated functionality into standalone class:

public class ExternalCacheDirManager {
    private final Context mContext;

    public ExternalCacheDirManager(Context context) {
        mContext = context;
    }

    public boolean isExternalCacheDirAvailable() {
        ...
    }

    public void storeInExternalCacheDir(String fileName, byte[] data) {
        ...
    }
}

Then we can remove dependency on Context from MyClass:

public class MyClass {
    private final ExternalCacheDirManager mExternalCacheDirManager;

    public MyClass(ExternalCacheDirManager externalCacheDirManager) {
        mExternalCacheDirManager = externalCacheDirManager;
    }

    public void doSomething() {
        if (mExternalCacheDirManager.isExternalCacheDirAvailable()) {
            ...
            mExternalCacheDirManager.storeInExternalCacheDir(fileName, data);
        }
    }
}

The benefits of this implementation are enormous:

  1. The code inside MyClass became more expressive and more readable. This alone is a huge win.
  2. All changes and enhancements related to external cache directory that might be required in the future will be contained withing this single class.
  3. The constructor clearly states which classes need to be substituted with test-doubles during unit testing.
  4. Since MyClass doesn’t have access to Context, it won’t become coupled to multiple Context responsibilities.

But I still pass Context into ExternalCacheDirManager class. Isn’t this against the Context Rule? Yes, it is, but I simply can’t completely work around the fact that Context objects are at the core of Android framework.

What I achieved with ExternalCacheDirManager is that now I expose just one single Context’s responsibility through a narrowly scoped API. Since the implementation of this class is under my control, the rest of the codebase is no longer coupled to Context in general.

As long as ExternalCacheDirManager has just one responsibility and doesn’t become a mess by itself, the project will be good.

Context as runtime parameter:

There is one case in which following Context Rule is impossible. This happens when Context object is required as a runtime parameter by code outside of our control.

For example, this class functions as an image loader by wraping around third-party image loading library:

public class ImageLoader {

    private final Activity mActivity;

    public ImageLoader(Activity activity) {
        mActivity = activity;
    }

    public void loadImage(String uri, ImageView target) {
        Glide.with(mActivity).load(uri).into(target);
    }
}

In this case Activity object (which extends Context) is required as a runtime argument for internal image loading library. Therefore, ImageLoader class must know about Activity by itself.

However, even though dependency on Context is inevitable in this case, note how ImageLoader converted Glide’s method argument into constructor argument. This is still a big win.

By employing this approach I reduced the number of method arguments used during image loading from three to two. This is very handy given the fact that this method might be called from tens of different places in the codebase. Such a reduction is very beneficial for long term readability and maintainability of the code.

Conclusion:

Alright, time to wrap it up.

I think that Context objects are very unhealthy dependencies. Whenever possible, I try to remove these dependencies completely. If not possible, I make sure to minimize the exposure of Context objects and wrap them inside classes under my control.

I call this mindset “Context Rule”.

In my experience, following Context Rule results in much cleaner codebases that contain classes with narrowly scoped API surfaces.

On the other hand, unrestricted usage of Context almost guarantees widespread violation of Single Responsibility Principle and high coupling between unrelated functionalities.

The choice is yours.

As always, you can leave comments and ask questions below, and please consider subscribing to my blog if you liked this article.

Check out my premium

Android Development Courses

5 comments on "Android Context Needs Isolation"

  1. This may seem like a subtle point, but making your code more compliant with “Law of Demeter” and less reliant on Context-passing per your suggestion may have a great impact in the long run and help avoid many quirks. Great post!

    Reply
  2. In last example (Context as runtime parameter), why not use Context from imageview?
    Glide.with(target.getContext()).load(uri).into(target);

    Reply
    • Hello Francisco and thanks for an interesting question.

      First of all, these approaches are not equivalent because the behavior of with(Context) and with(Activity) methods is different.

      However, even if it would be the same I wouldn’t do this. This is basically using View object as a proxy, which constitutes a violation of Law of Demeter. It is much better for long term readability and maintainability to explicitly inject the required arguments rather than retrieving them internally.

      Reply
    • Hi,
      It depends on which kind of Context is used and what’s the scope of the other class.
      In general, if you store a reference to a short living object inside long living object, then you might produce a memory leak (not limited to Context).
      In Android, you want to avoid passing references to “short-lived Context” (Activity, Service) into classes that “live” in Application scope. However, you can pass Application object into any class and it’s safe.
      In this respect, you might want to read my article about Liskov Substitution Principle. I discuss there this issue of memory leaks in a bit more details.

      Reply

Leave a Comment