---
title: "DexClassLoader, or loading part of an app from the network"
url: "https://romankryvolapov.com/en/dex-class-loader/"
description: "How an Android app downloads a .dex file from a server and loads classes from it with DexClassLoader, what Android 14 restricts, and why Google Play forbids it."
language: en
published: 2021-01-21
updated: 2026-11-05
---
I once wrote a small program that downloads precompiled `.dex` code from a server and runs it. I wanted to find out whether part of an app could live not inside the APK but as code on a server, loaded while the app is running.

The idea came from an article in the Russian magazine Xakep about modules for Android trojans ([part 2](https://xakep.ru/2016/07/05/android-plugins-binder-2/)). Trojans were not the point: I was curious how the mechanism itself works and what Google Play would say about it.

## How it works

After a build, an Android app's code sits in `.dex` files — bytecode for the ART virtual machine. Normally all of them are packed into the APK and loaded by the system class loader. But Android also has public class loaders that accept a file of your own:

- `DexClassLoader` loads classes from a `.dex`, `.jar` or `.apk` file on disk;
- `PathClassLoader` does the same, and the system uses it for the app itself;
- `InMemoryDexClassLoader` (API 26+) loads a `.dex` straight from a `ByteBuffer`, with no file at all.

A loaded class is no different from one of your own: you can instantiate it, call its methods and pass it a context. The exception is app components: an Activity or a Service from such code is not declared in the manifest, so the system will not start it.

## The code

The cleanest way to connect a plugin to the app is a shared interface: the interface lives in the app, and the class inside the `.dex` implements it. After loading, the class is cast to the interface and called without reflection.

```java
File dexFile = new File(getCodeCacheDir(), "plugin.dex");
if (dexFile.exists()) {
    dexFile.delete();
}

try (InputStream input = new URL(baseUrl + "plugin.dex").openStream();
     FileOutputStream output = new FileOutputStream(dexFile)) {
    // Android 14+: the file must become read-only before anything is written to it
    dexFile.setReadOnly();
    byte[] buffer = new byte[8192];
    int count;
    while ((count = input.read(buffer)) != -1) {
        output.write(buffer, 0, count);
    }
}

DexClassLoader loader = new DexClassLoader(
        dexFile.getAbsolutePath(),
        null,               // optimizedDirectory: ignored since API 26
        null,               // native library path, if the plugin needs one
        getClassLoader());  // parent: lets the plugin see the app's classes

Class<?> pluginClass = loader.loadClass("com.example.plugin.PluginImpl");
Plugin plugin = (Plugin) pluginClass.getDeclaredConstructor().newInstance();
plugin.run(getApplicationContext());
```

Without a shared interface, reflection is all that is left — that is what my first version did:

```java
for (Method method : pluginClass.getDeclaredMethods()) {
    method.setAccessible(true);
    Log.d(TAG, "method: " + method.getName());
}
for (Field field : pluginClass.getDeclaredFields()) {
    field.setAccessible(true);
    Log.d(TAG, "field: " + field.getName());
}
```

## Building the .dex

The plugin is compiled with plain `javac` or `kotlinc` against the same `Plugin` interface, and the resulting `.class` files are converted to `.dex` with `d8` from the Android SDK (the `build-tools` folder). `d8` needs an existing output folder, `android.jar` from the SDK, and on the classpath the compiled `Plugin` interface, which stays in the app and does not go into the plugin:

```bash
mkdir -p out
d8 --lib "$ANDROID_HOME/platforms/android-36/android.jar" --classpath build/classes \
  --output out/ build/classes/com/example/plugin/PluginImpl.class
```

`out/` will contain `classes.dex` — that is the file to put on the server.

In a release build the interface also has to survive R8: the plugin is compiled against its original names, and the rule `-keep interface com.example.app.Plugin { *; }` keeps them.

## Limitations

**Android 14.** If the app targets API 34 or higher, every dynamically loaded file must be read-only, otherwise the system throws an exception when loading it. Mark the file read-only right after opening it, before writing its contents, as in the code above. A file that already exists is better deleted and downloaded again. Details are in the [Android 14 behavior changes](https://developer.android.com/about/versions/14/behavior-changes-14).

**Google Play.** In the first version of this note I wrote that once an app has passed review, it can load anything onto the device. That is wrong. Google Play's [Device and Network Abuse](https://support.google.com/googleplay/android-developer/answer/16559646) policy explicitly forbids apps and the SDKs inside them from downloading executable code — `.dex`, `.jar`, `.so` — from any source other than Google Play itself. Review may let such an app through, but it is a policy violation and grounds for removal. The exception is code that runs in a virtual machine or interpreter without direct access to Android APIs, such as JavaScript in a WebView. The sanctioned way to deliver parts of an app on demand is [Play Feature Delivery](https://developer.android.com/guide/playcore/feature-delivery).

**Security.** Loaded code gets all of the app's permissions. So: HTTPS only, and before loading, verify the file's signature with a public key built into the APK — otherwise a file swapped on the server or in transit turns into someone else's code running inside your app. A hash fetched from the same server proves nothing, since whoever swaps the file swaps the hash too. Google's general [advice](https://developer.android.com/privacy-and-security/risks/dynamic-code-loading) is to avoid dynamic code loading unless there is a business need.

Where it still makes sense: enterprise apps distributed outside Google Play, plugin systems in internal tools, experiments, and studying how class loading works in ART.
