The Context Class Hierarchy: ContextWrapper, ContextImpl, and ContextThemeWrapper

skydovesJaewoong Eum (skydoves)||17 min read

The Context Class Hierarchy: ContextWrapper, ContextImpl, and ContextThemeWrapper

Every Android developer uses Context constantly. You read LocalContext.current inside a composable, call getApplicationContext() to avoid leaking when you build an image loader or a DataStore, resolve a string with stringResource(), and ask for a system service with context.getSystemService(). The surface is so familiar that the structure underneath is easy to ignore: an Activity is a Context, an Application is a Context, and a Service is a Context, yet they have different lifetimes, different themes, and different resource configurations. They cannot all reimplement the same large abstract API. The deeper question is what actually implements Context, and why the framework splits that implementation across a chain of wrapper classes instead of a single class.

In this article, you'll dive deep into the internal structure of Context, exploring the abstract Context contract, how ContextWrapper delegates to a base through attachBaseContext, why ContextImpl is the one class that does the real work and what it holds, how ContextThemeWrapper adds a per context theme and resources, how Activity composes all three layers in attach(), how getSystemService resolves through SystemServiceRegistry, and how derived contexts like createConfigurationContext are built without leaking into the rest of the app.

The fundamental problem: One interface, many component types

When you call context.getString(R.string.title) or context.getSystemService(Context.WINDOW_SERVICE), you are talking to an interface that does a lot of unrelated work: resource lookup, asset access, theme resolution, system service access, file and database paths, permission checks, starting activities, and binding services. Now consider that this same API must be satisfied by an Application that lives for the entire process, by an Activity that has its own theme and a configuration tied to a window, and by a Service that has neither a theme nor a window.

A naive design would implement the Context API separately in Activity, Application, and Service. That fails immediately. The logic for loading a resource or fetching a system service is identical regardless of which component asks for it, so copying it into every component class would mean three copies of a large, intricate implementation to keep in sync. It would also make it impossible to wrap a Context to adjust one behavior, such as overriding the locale, without reimplementing everything else.

The framework solves this with a separation. There is exactly one class that implements the Context API for real, ContextImpl. Components do not extend it. Instead they extend ContextWrapper, a decorator that holds a ContextImpl and forwards every call to it. Components that need a theme extend ContextThemeWrapper, which inserts one more layer. The result is that Activity, Application, and Service reuse a single implementation while still being usable as a Context themselves.

Context: The abstract contract

Context itself is an abstract class. Almost every method on it is abstract, including getResources(), getAssets(), getTheme(), getSystemService(String), getApplicationContext(), startActivity(Intent), and the file and database accessors. It defines the contract and the well known service name constants like WINDOW_SERVICE and LAYOUT_INFLATER_SERVICE, but it provides no behavior.

Because Context declares the API and nothing else, the interesting question is never "what does Context do". It is "which object is on the other end of these abstract calls". For any Context you hold in app code, the answer is almost always a ContextImpl, reached through one or two wrappers.

ContextWrapper: A decorator that delegates to a base

ContextWrapper is the layer your component classes actually extend. Its entire job is to hold another Context and forward to it. Looking at the core of the class:

public class ContextWrapper extends Context {
    @UnsupportedAppUsage
    Context mBase;

    public ContextWrapper(Context base) {
        mBase = base;
    }

    protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }
}

There are two ways the base gets set. A ContextWrapper can receive it through the constructor, or the framework can install it later through attachBaseContext. The guard in attachBaseContext is the reason it can only be set once: once mBase is non null, a second attach throws IllegalStateException("Base context already set"). This matters for components like Activity, which are instantiated by the framework with the no argument path and have their base context attached afterward, during attach(). It is also why overriding attachBaseContext() in an Activity to wrap the context (a common technique for per app locale) must call super.attachBaseContext() exactly once with the wrapped context.

Every functional method on ContextWrapper simply forwards. Two representative examples:

@Override
public Context getApplicationContext() {
    return mBase.getApplicationContext();
}

@Override
public Object getSystemService(String name) {
    return mBase.getSystemService(name);
}

Notice the structure: ContextWrapper adds no behavior of its own. It is a decorator whose value is the indirection itself. Because your Activity is a ContextWrapper, you can subclass it, and because the real work lives behind mBase, the framework can swap or wrap that base without your component noticing. The forwarding is mechanical for every method except the few that subclasses like ContextThemeWrapper choose to override.

ContextImpl: Where the work actually happens

ContextImpl is the class on the other end of mBase. It is package private in android.app, so you can never name it in app code, but every Context you touch bottoms out here. Instead of forwarding, it holds the state needed to do the work:

class ContextImpl extends Context {
    @UnsupportedAppUsage
    final @NonNull LoadedApk mPackageInfo;
    private final @NonNull ResourcesManager mResourcesManager;
    @UnsupportedAppUsage
    private Resources mResources;
    private Context mOuterContext;
    final Object[] mServiceCache = SystemServiceRegistry.createServiceCache();
}

Each field maps to one responsibility:

  • mPackageInfo: the LoadedApk, which represents your loaded APK. It owns the ClassLoader, the application info, the resource directories, and a reference to the singleton Application.
  • mResourcesManager and mResources: the resource stack. ResourcesManager is a process wide cache that hands out Resources objects rebased on a given configuration, and mResources is the one this particular context resolves through.
  • mOuterContext: a back reference to the outermost wrapper. When a ContextImpl is the base of an Activity, mOuterContext points back at the Activity. System services use this so a service receives the public facing context, not the hidden implementation.
  • mServiceCache: the per context cache of already created system services, indexed by SystemServiceRegistry.

This article continues for subscribers

Subscribe to Dove Letter for full access to 40+ deep-dive articles about Android and Kotlin development.

Become a Sponsor