Retroflector is a compile-time + runtime reflection bridge for hidden Android/framework APIs. You define Java/Kotlin stub interfaces with @Proxy* annotations; KSP generates *Proxy accessors and *Context/*Static proxy interfaces. At runtime, Retroflector turns those interfaces into JDK dynamic proxies backed by real reflection.
- Setup
- Retroflector
- Stub Annotations
- KSP Generated Code
- ClassUtil
- Reflector
- JavaReflect
- Usage Example
- Notes
Add Retroflector as a dependency and enable the KSP processor in the module that owns your stub interfaces:
dependencies {
implementation(project(":Retroflector"))
ksp(project(":Retroflector"))
}Stub interfaces live in your app/module source. Generated Kotlin files land in build/generated/ksp/<variant>/kotlin/.
Retroflector is the main runtime entry point. It builds JDK proxies for generated *Context and *Static interfaces and dispatches calls to hidden fields/methods/constructors via Reflector.
@JvmField
var DEBUG: Boolean
@JvmField
var CACHE: Boolean- DEBUG — when
true, reflection failures print stack traces before fallback/throw behavior runs. - CACHE — reserved flag (currently unused by proxy cache; static proxies are always cached per interface class).
@JvmStatic
fun <T> create(clazz: Class<T>, caller: Any?, withException: Boolean): T?- Description: Creates (or returns cached) proxy for
clazz. - Parameters:
clazz— generated*Contextor*Staticinterface class.caller— instance to bind for instance calls; passnullfor static access.withException— whentrue, reflection errors propagate; whenfalse, returns default/null for reference types (primitives may throwNullPointerException).
- Returns: Proxy instance, or
nullif target class cannot be resolved.
Annotate stub interfaces in source. KSP reads these at compile time; runtime reads @ProxyClassName / @ProxyClass / generated @ProxyClassNameRef.
| Annotation | Target | Purpose |
|---|---|---|
@ProxyClassName("fqcn") |
class | Map stub to real class by name |
@ProxyClass |
class | Map stub to real KClass |
@ProxyField |
function | Instance field getter |
@ProxyStaticField |
function | Static field getter |
@ProxyMethod |
function | Instance method |
@ProxyStaticMethod |
function | Static method |
@ProxyConstructor |
function | Constructor |
@ProxyParamClass |
parameter | Override reflected param type |
@ProxyParamClassName |
parameter | Override reflected param type by name |
Generated interfaces also use @ProxyFieldRef, @ProxyFieldSet, @ProxyFieldCheck, @ProxyMethodCheck, @ProxyConstructorRef — you do not hand-write these.
Example stub:
@ProxyClassName("android.app.Activity")
public interface Activity {
@ProxyField ActivityInfo mActivityInfo();
@ProxyMethod void onActivityResult(int a, int b, Intent data);
}For each @ProxyClassName / @ProxyClass stub Foo, KSP emits:
| File | Role |
|---|---|
FooProxy.kt |
Entry object with get(), getWithException(), get(caller), getRealClass() |
FooContext.kt |
Instance fields/methods/constructors |
FooStatic.kt |
Static fields/methods/constructors |
FooProxy.get(caller) returns FooContext. FooProxy.get() returns FooStatic. Static field getters are generated as nullable boxed types (Int?, etc.) so Java callers can null-check when a field is missing on a device/API level.
Resolves the real Class<*> behind a stub or generated interface.
@JvmStatic
fun classReady(clazz: Class<*>): Class<*>?- Description: Walks
@ProxyClassNameRef,@ProxyClass, or@ProxyClassNameto load the target class. - Returns: Resolved class, or
nullif not found.
Low-level fluent wrapper around java.lang.reflect for direct use outside generated proxies. Also powers Retroflector internally.
@JvmStatic
fun on(type: Class<*>): Reflector
@JvmStatic
@Throws(Exception::class)
fun on(name: String): Reflector
@JvmStatic
@Throws(Exception::class)
fun with(value: Any?): Reflector- on(Class) / on(String) — start a chain on a type.
- with(Any?) — bind an instance caller.
Reflector.QuietReflector exposes the same API but swallows errors and returns null.
| Method | Description |
|---|---|
field(name) |
Select field (walks supers) |
get() / get(target) |
Read field |
set(value) / set(target, value) |
Write field |
method(name, *paramTypes) |
Select method |
call(*args) / callByCaller(target, *args) |
Invoke method |
constructor(*paramTypes) |
Select constructor |
newInstance(*args) |
Construct instance |
bind(value) / unbind() |
Set/clear default caller |
getField() / getMethod() |
Expose resolved Field / Method |
All non-quiet methods throw Exception on failure.
Legacy shim that forwards to Retroflector. Use Retroflector in new code.
JavaReflect.create(clazz, caller, withException)
JavaReflect.DEBUG
JavaReflect.CACHE1. Define stub (src/main/java/miaw/android/app/Activity.java):
@ProxyClassName("android.app.Activity")
public interface Activity {
@ProxyField ActivityInfo mActivityInfo();
}2. Use generated bridge (after build):
import miaw.android.app.ActivityProxy
val activity = someActivityInstance
val info = ActivityProxy.get(activity).mActivityInfo()
// static side
val realClass = ActivityProxy.getRealClass()ActivityInfo info = ActivityProxy.get(activity).mActivityInfo();
Class<?> cls = ActivityProxy.getRealClass();3. Direct reflection (no stub codegen):
val value = Reflector.on("android.os.SystemProperties")
.method("get", String::class.java, String::class.java)
.call<String>("ro.build.version.sdk", "0")- Stub interfaces must use the
@Proxy*annotations; KSP only processes annotated members. - Pass
withException = true(getWithException()) when failures should throw instead of returning null/defaults. - Instance proxies hold a weak reference to
caller; if the caller is GC'd, instance calls fall back to null/default behavior. - Raw Java generic types in stubs (e.g.
Listwithout type args) are emitted as star-projected Kotlin types (MutableList<*>).