exteraGram

Class Proxy

Create Java subclasses and dynamic proxy classes from Python.

Class Proxy

The class proxy API allows a plugin to create a real Java class at runtime and implement it in Python.

This is useful when a Java API expects:

  • a subclass of an existing Java class
  • an implementation which overrides parent methods
  • extra methods declared directly on the generated Java class
  • Java fields stored on the generated object

The proxy system is built on top of DexMaker, so the result is an actual Java class which can be passed anywhere the app expects that type.

new_instance() returns a Python peer object, not the raw Java instance. Use .java when an API explicitly requires the generated Java object itself.

The recommended API is the managed DSL from extera_utils.classes.

from extera_utils.classes import (
    Base,
    java_subclass,
    joverride,
    joverload,
    jmethod,
    jMVELmethod,
    jMVELoverride,
    jclassbuilder,
    jfield,
    jgetmethod,
    jsetmethod,
    jconstructor,
    jpreconstructor,
)

Main pieces:

  • Base: base Python class for managed Java subclasses
  • @java_subclass(JavaClass, Interface1, Interface2, ..., methods=..., constructors=...): binds your Python class to a Java base class and optional Java interfaces
  • @joverride(...): overrides an existing Java method
  • @joverload(name, arg_types): overload-friendly alias for @joverride
  • @jmethod(...): adds a new method directly to the generated Java class
  • jMVELmethod(...): adds a Java method backed by MVEL instead of Python
  • jMVELoverride(...): overrides a parent Java method with MVEL code
  • @jclassbuilder(): lets you modify DexMaker before the class is finalized and loaded
  • jfield(...): adds a Java field to the generated class
  • jgetmethod(...) / jsetmethod(...): generate Java-only getter/setter methods for a field
  • @jconstructor(...): runs Python code after the Java constructor has finished
  • @jpreconstructor(...): modifies constructor arguments before calling the Java parent constructor

Useful aliases also exist:

  • jmvelmethod / jMVELmethod
  • jmveloverride / jMVELoverride
  • joverride, joverload, jmethod, jfield, jconstructor, jpreconstructor
  • jgetmethod, jsetmethod, jclassbuilder

Quick Start

from extera_utils.classes import Base, java_subclass, joverload, jfield
from java.util import ArrayList
 
 
@java_subclass(ArrayList)
class CountingList(Base):
    added_count = jfield("int", default=0)
 
    @joverload("add", ["java.lang.Object"])
    def add_item(self, value):
        self.added_count += 1
        return super().add_item(value)
 
 
items = CountingList.new_instance()
items.add("Hello")
items.add("world")

What happens here:

  • CountingList becomes a generated Java subclass of java.util.ArrayList
  • added_count becomes a real Java field on that class
  • add(Object) is overridden in Java and routed into Python
  • super().add_item(...) calls the original parent Java implementation

Because ArrayList.add is overloaded, the example uses @joverload(...). For overloaded Java methods, prefer explicit signatures instead of bare @joverride().

Creating a Java Subclass

Use @java_subclass for the most readable syntax:

@java_subclass(SomeJavaClass)
class MySubclass(Base):
    ...

If the generated Java class must also implement interfaces, pass them after the base class:

@java_subclass(FrameLayout, CustomDelegate1, CustomDelegate2)
class MyCell(Base):
    ...

This produces a generated Java class equivalent to:

class GeneratedProxy extends FrameLayout implements CustomDelegate1, CustomDelegate2

Generated proxy classes are also loaded through a shared generated-class loader chain, so a proxy created later can reference earlier generated proxy classes when needed.

This is still not a substitute for a stable API type. If possible, prefer referencing another proxy through an interface or a common base class instead of depending on the exact generated class.

You can also bind after the class definition:

class MySubclass(Base):
    ...
 
 
MySubclass.bind(SomeJavaClass)

And bind(...) supports the same interface list:

MySubclass.bind(FrameLayout, CustomDelegate1, CustomDelegate2)

To get the generated Java class:

proxy_java_class = MySubclass.java_class()

To create an instance:

instance = MySubclass.new_instance()
instance2 = MySubclass.new_instance("text", 123)  # provide java-constructor args

instance is a Python peer of MySubclass. If you need the raw Java object:

java_instance = instance.java
java_instance2 = MySubclass.new_java_instance("text", 123)

If you want to pass separate arguments into the Python __init__ method, use init_args:

instance = MySubclass.new_instance("java ctor value", init_args=["python init value"])

In this case:

  • "java ctor value" is used for Java constructor resolution
  • ["python init value"] is passed to Python __init__

If you already have a raw Java instance of the generated class and want to recover its Python peer, use from_java(...):

existing_java = MySubclass.new_java_instance("text")
peer = MySubclass.from_java(existing_java)

This is useful when:

  • some Java API created or stored the proxy instance for you
  • you received the generated object back through another callback
  • you want to continue working with the Python peer instead of the raw Java object

Overriding Methods

Simple Override

If the Java method name is the same as the Python method name:

@java_subclass(SomeJavaClass)
class MySubclass(Base):
    @joverride()
    def onClick(self, view):
        print("Clicked")

Override With Another Python Name

If the Java method name is not convenient in Python, pass it explicitly:

@java_subclass(SomeJavaClass)
class MySubclass(Base):
    @joverride("equals")
    def equals_(self, a, b):
        return a == b

This is useful for names such as equals, hashCode, or when you want several Python handlers for overloaded methods.

Overloaded Methods

When a Java class has several methods with the same name, use the overload-aware decorators.

@java_subclass(SomeJavaClass)
class MySubclass(Base):
    @joverload("setValue", ["int"])
    def set_value_int(self, value):
        print("int overload", value)
 
    @joverload("setValue", ["java.lang.String"])
    def set_value_text(self, value):
        print("string overload", value)

You can write the same thing with joverride:

@joverride("setValue", ["int"])
def set_value_int(self, value):
    ...

Supported Type Formats

Argument and return types may be specified as:

  • primitive names like "int", "boolean", "long"
  • fully-qualified class names like "java.lang.String"
  • Java classes returned by jclass(...)
  • Java primitive type objects

Example:

from java import jclass
 
View = jclass("android.view.View")
 
@joverload("setView", [View])
def set_view(self, view):
    ...

Calling Parent Methods With super()

For overridden methods, super() works like a normal Python method call:

@java_subclass(SomeJavaClass)
class MySubclass(Base):
    @joverride()
    def onClick(self, view):
        print("Before parent")
        result = super().onClick(view)
        print("After parent")
        return result

For overloaded methods, call the Python method name you defined:

@java_subclass(SomeJavaClass)
class MySubclass(Base):
    @joverload("setValue", ["int"])
    def set_value_int(self, value):
        return super().set_value_int(value)

Adding New Java Methods

Use @jmethod to declare a method which does not exist on the parent Java class.

@java_subclass(SomeJavaClass)
class MySubclass(Base):
    @jmethod("debugLabel", "java.lang.String", ["int"])
    def build_debug_label(self, value):
        return f"value={value}"

This creates a real Java method named debugLabel(int) on the generated class.

jmethod Signatures

These forms are supported:

@jmethod("java.lang.String", ["int"])
def my_method(self, value):
    ...
 
@jmethod("debugLabel", "java.lang.String", ["int"])
def build_debug_label(self, value):
    ...

If the return type and argument types are not passed explicitly, jmethod can infer them from Python annotations:

@jmethod()
def build_debug_label(self, value: "int") -> "java.lang.String":
    return f"value={value}"

Explicit types are still recommended when the Java signature matters a lot.

Adding MVEL Methods

If a method is called very often and the logic is simple enough to keep on the Java side, you can generate a method whose body is executed by MVEL instead of calling Python.

from extera_utils.classes import Base, java_subclass, jMVELmethod
 
 
@java_subclass(SomeJavaClass)
class MySubclass(Base):
    debug_label = jMVELmethod(
        return_type="java.lang.String",
        arguments=[("prefix", "java.lang.String"), ("value", "int")],
        code="""
            return prefix + ": value=" + value + ", class=" + java.getClass().getSimpleName();
        """,
    )

This creates a real Java method named debug_label(String, int) on the generated class, but its implementation runs through compiled MVEL.

Overriding With MVEL

For parent methods, use jMVELoverride(...):

from extera_utils.classes import Base, java_subclass, jMVELoverride
 
 
@java_subclass(SomeFactoryClass)
class OptimizedFactory(Base):
    isClickable = jMVELoverride(
        arguments=[],
        code="return factory_is_clickable;",
    )

If the parent method is overloaded, pass explicit argument types just like @joverload(...):

@java_subclass(SomeJavaClass)
class MySubclass(Base):
    add = jMVELoverride(
        arguments=[("value", "java.lang.Object")],
        code="""
            add_calls = add_calls + 1;
            return SUPER_add(value);
        """,
    )

MVEL Context

Inside MVEL:

  • the root object is the generated Java instance, so Java fields and methods can be accessed directly
  • java: the same generated Java instance
  • python / py / self: the attached Python peer object
  • args: Object[] with all method arguments
  • argc: number of arguments
  • arg0, arg1, ...: positional arguments
  • named arguments from arguments=[("name", "type"), ...]

That means all of these are valid:

@java_subclass(SomeJavaClass)
class MySubclass(Base):
    describe = jMVELmethod(
        return_type="java.lang.String",
        arguments=[("value", "int")],
        code="""
            return "value=" + value + ", class=" + java.getClass().getSimpleName();
        """,
    )

For override methods, the generated SUPER_<methodName>(...) bridge is also available:

@java_subclass(SomeViewClass)
class MyView(Base):
    onMeasure = jMVELoverride(
        arguments=[("widthMeasureSpec", "int"), ("heightMeasureSpec", "int")],
        code="""
            SUPER_onMeasure(
                android.view.View$MeasureSpec.makeMeasureSpec(
                    android.view.View$MeasureSpec.getSize(widthMeasureSpec),
                    android.view.View$MeasureSpec.EXACTLY
                ),
                android.view.View$MeasureSpec.makeMeasureSpec(
                    org.telegram.messenger.AndroidUtilities.dp(68),
                    android.view.View$MeasureSpec.EXACTLY
                )
            );
            return null;
        """,
    )

That pattern is useful for small hot-path UI overrides where the method body is simple but called very often.

When To Use MVEL

  • Use jfield(..., methods=[jgetmethod(...), jsetmethod(...)]) for pure field getter/setter hot paths. That is still the fastest option.
  • Use jMVELmethod(...) or jMVELoverride(...) when the method needs a bit of logic, but you still want to avoid calling into Python every time.
  • Keep MVEL bodies small and predictable. If the logic needs Python objects, complex state, or heavy control flow, regular Python overrides are usually easier to maintain.

Adding Java Fields

Use jfield to declare fields on the generated Java class.

@java_subclass(SomeJavaClass)
class MySubclass(Base):
    counter = jfield("int", default=0)
    title = jfield("java.lang.String", default="Hello")

You can read and write them like normal Python attributes:

self.counter += 1
self.title = "Updated"

Notes:

  • the field is generated on the Java class
  • the value is available through the Python peer object as a normal attribute
  • assigning to the field updates the attached Java instance when possible

Java-only Field Accessors

If a method is called very often and it only needs to return or update a field value, you can generate a Java-only getter or setter directly from jfield(...).

from extera_utils.classes import jfield, jgetmethod, jsetmethod
 
 
@java_subclass(SomeJavaClass)
class MySubclass(Base):
    shadow_value = jfield(
        "boolean",
        default=False,
        methods=[
            jgetmethod("isShadow"),
            jsetmethod("setShadow"),
        ],
    )

This generates:

  • a Java field named shadow_value
  • a Java method isShadow() which directly returns that field
  • a Java method setShadow(boolean) which directly writes that field

These accessor methods do not call into Python at all, so they are useful for very hot paths such as repeated UI queries.

This is especially useful when you want to override a parent getter with a static value:

@java_subclass(SomeFactoryClass)
class OptimizedFactory(Base):
    is_shadow_value = jfield(
        "boolean",
        default=False,
        methods=[jgetmethod("isShadow")],
    )

If SomeFactoryClass already has isShadow(), the generated Java method overrides it in the proxy class and returns the field immediately.

Because generated proxy classes now load through a shared generated-class loader chain, a proxy created later can also reference a proxy class created earlier as a field or method type if you truly need that.

Even so, a stable shared type such as an interface or common base class is still the safer design for public APIs.

Constructors

Constructor hooks are split into two stages:

  • @jpreconstructor(...): before the parent Java constructor call
  • @jconstructor(...): after the Java object has already been created

Pre-constructor

Use this when you need to change constructor arguments:

@java_subclass(SomeJavaClass)
class MySubclass(Base):
    @jpreconstructor(["java.lang.String"])
    def normalize_text(cls, text):
        return [text.strip()]

The function should return:

  • None to keep original arguments
  • a list or tuple with replacement arguments
  • a single value for a one-argument constructor

Post-constructor

Use this when the Java object already exists and you want to initialize Python state:

@java_subclass(SomeJavaClass)
class MySubclass(Base):
    initialized = jfield("boolean", default=False)
 
    @jconstructor(["java.lang.String"])
    def init_from_text(self, text):
        self.initialized = True
        self.last_text = text

__init__ and on_post_init

The managed DSL also supports generic initialization:

@java_subclass(SomeJavaClass)
class MySubclass(Base):
    def __init__(self, text):
        self.python_only_state = text
 
    def on_post_init(self, text):
        print("Java object is ready:", self.java)

Initialization order is:

  1. @jpreconstructor(...) if matched
  2. Java parent constructor
  3. Python __init__(...)
  4. matched @jconstructor(...)
  5. on_post_init(...)

If you only need a single generic hook, __init__ is usually enough.

If you need different logic for overloaded constructors, use @jconstructor(...).

Separate Python init arguments

By default, Python __init__ receives the same arguments as the Java constructor.

If you need different Python-only initialization data, pass it explicitly through new_instance(..., init_args=[...]):

@java_subclass(SomeJavaClass)
class MySubclass(Base):
    def __init__(self, title, debug_mode):
        self.title = title
        self.debug_mode = debug_mode
 
 
instance = MySubclass.new_instance(
    "java constructor text",
    init_args=["Python title", True],
)

This is useful when the Java constructor signature and the Python peer state do not match exactly.

Accessing the Attached Java Instance

Inside a managed subclass:

  • self.java is the attached Java instance
  • self.this is an alias to self.java

Example:

@java_subclass(SomeJavaClass)
class MySubclass(Base):
    @joverride()
    def onAttached(self):
        print(self.java.getClass().getName())

If Python cannot find an attribute on the peer object, it falls back to the Java instance:

self.getContext()
self.hashCode()

This means most Java calls work directly on the peer object:

instance = MySubclass.new_instance()
instance.hashCode()
instance.getClass().getName()

But if a Java API checks the exact runtime type and expects a real Java object parameter, pass instance.java.

Wrapping Existing Java Instances

Sometimes an API hands your generated object back to you as a plain Java object. In that case, use from_java(...) to recover the Python peer instead of manually copying state around.

@java_subclass(SomeJavaClass)
class MySubclass(Base):
    @joverride()
    def onAttached(self):
        print("Attached!", self.java)
 
 
peer = MySubclass.new_instance()
raw_java = peer.java
 
# Later, possibly in another callback:
same_peer = MySubclass.from_java(raw_java)
same_peer.onAttached()

If the peer already exists, from_java(...) returns the same Python object. If it does not, the proxy system creates and binds it.

Passing Python Objects Through Java

The low-level helper PyObj is available when you need to carry an arbitrary Python object through Java-facing APIs.

from extera_utils.classes import PyObj
 
 
payload = PyObj.create({"debug": True, "label": "factory args"})

PyObj is mainly useful when:

  • a Java API needs to carry opaque plugin-owned state
  • you are building low-level adapters or factory layers
  • you want to place Python data into a Java field or something like UItem.object

For normal subclassing and method overrides, you usually do not need PyObj directly.

Java Helper (J / JavaHelper / ClassHelper)

At the bottom of extera_utils.classes there is also a small reflection helper:

from extera_utils.classes import J, JavaHelper, ClassHelper

All three names point to the same class. In normal code, J(...) is the shortest and most convenient form.

What it is for

J(obj) wraps a Java object and gives you a more Python-friendly way to:

  • call reflected Java methods
  • read or write reflected Java fields
  • optionally prefer Java getters and setters over direct field access
  • optionally access non-public members
  • optionally ignore method return values so calls can be chained

This helper is meant for convenience when you already have a Java object and want a compact reflection-style API.

It is not a replacement for:

  • hook_utils.find_class(...) when you first need to locate a Java class
  • the managed proxy DSL when you need real Java subclasses
  • overload-aware reflection when exact method signatures matter

Basic usage

from extera_utils.classes import J  # or JavaHelper
 
 
helper = J(some_java_object)

From there:

  • helper.someField reads a reflected field, or getSomeField() if getter/setter mode is enabled and such a getter exists
  • helper.someField = value writes a reflected field, or calls setSomeField(value) if setter/getter mode is enabled and such a setter exists
  • helper.someMethod(...) reflects a Java method and invokes it

Getter/setter mode

By default, J(...) prefers JavaBean-like getters and setters when they exist.

So this:

title = J(view).title
J(view).title = "Updated"

roughly means:

title = view.getTitle()
view.setTitle("Updated")

if getTitle() / setTitle(...) are declared on the class.

If no matching getter/setter exists, the helper falls back to direct field access.

Example

from extera_utils.classes import J
 
 
chat_cell = ...
cell = J(chat_cell)
 
# If the class declares getMessageObject(), this resolves through the getter.
message_object = cell.messageObject
 
# If the class declares setHighlighted(boolean), this resolves through the setter.
cell.highlighted = True
 
# Regular method calls work too.
cell.requestLayout()

Accessing non-public members

By default, non-public reflected members are blocked.

To allow access to private, protected, or package-private declared members, switch to JAccessAll:

hidden = J(obj).JAccessAll.secretField
J(obj).JAccessAll.secretMethod()

This causes the helper to call setAccessible(True) on the reflected member before using it.

To explicitly switch that back off, use JNotAccessAll.

Disabling getter/setter mode

If you want raw field access instead of getX() / setX(...) resolution, switch to JNotUseGetterAndSetter:

helper = J(obj).JNotUseGetterAndSetter
 
raw_value = helper.someField
helper.someField = 123

To explicitly switch it back on, use JUseGetterAndSetter.

Ignoring return values for chaining

Normally, a reflected method call returns the Java result:

result = J(obj).setTitle("Hello")

If you want fluent chaining instead, use JIgnoreResult:

J(obj).JIgnoreResult.setTitle("Hello").requestLayout().invalidate()

In that mode, each reflected method call returns the helper itself instead of the Java return value.

To switch back to normal behavior, use JNotIgnoreResult.

Flags are immutable wrappers

The special toggles:

  • JAccessAll / JNotAccessAll
  • JUseGetterAndSetter / JNotUseGetterAndSetter
  • JIgnoreResult / JNotIgnoreResult

do not mutate the existing helper in place.

Instead, each one returns a new helper wrapper with the updated behavior:

base = J(obj)
raw = base.JNotUseGetterAndSetter
private_raw = raw.JAccessAll

Static members

If the reflected field or method is static, the helper automatically uses None as the receiver when invoking it.

That means static members declared on the wrapped object's runtime class can still be reached through the same wrapper.

Important limitations

J(...) is intentionally lightweight, so there are a few constraints:

  • it indexes methods by name only, so overloaded Java methods are not disambiguated safely
  • it uses getDeclaredMethods() and getDeclaredFields(), so the helper focuses on members declared on the concrete runtime class
  • if it does not find a declared reflected member, it falls back to normal Chaquopy attribute access on the original object

Because of that, J(...) is best for simple convenience access, not for overload-sensitive or heavily dynamic reflection code.

If you need exact overload control, use normal Java reflection directly:

clazz = obj.getClass()
method = clazz.getDeclaredMethod("setValue", jint)
method.setAccessible(True)
method.invoke(obj, 5)

Practical example

from android_utils import log
from extera_utils.classes import J
 
 
def debug_view(view):
    helper = J(view)
 
    # Use getter if present.
    log(f"class = {helper.getClass().getName()}")
 
    # Reach non-public members only through the explicit access-all wrapper.
    try:
        hidden_tag = helper.secretTag
    except AttributeError:
        hidden_tag = helper.JAccessAll.secretTag
 
    log(f"hidden_tag = {hidden_tag}")
 
    # Switch to chain mode for void-style mutators.
    helper.JIgnoreResult.requestLayout().invalidate()

Full Example

from extera_utils.classes import (
    Base,
    java_subclass,
    joverride,
    joverload,
    jmethod,
    jfield,
    jconstructor,
    jpreconstructor,
)
 
 
@java_subclass(SomeJavaClass)
class DemoProxy(Base):
    counter = jfield("int", default=0)
    title = jfield("java.lang.String", default="Demo")
 
    @jpreconstructor(["java.lang.String", "int"])
    def normalize_args(cls, title, counter):
        return [title.strip(), max(0, counter)]
 
    def __init__(self, title, counter):
        self.python_state = {"created_with": title}
 
    @jconstructor(["java.lang.String", "int"])
    def init_fields(self, title, counter):
        self.title = title
        self.counter = counter
 
    @joverride()
    def onAttached(self):
        self.counter += 1
        return super().onAttached()
 
    @joverload("setValue", ["int"])
    def set_value_int(self, value):
        self.counter = value
        return super().set_value_int(value)
 
    @joverload("setValue", ["java.lang.String"])
    def set_value_text(self, value):
        self.title = value
 
    @jmethod("debugLabel", "java.lang.String", ["int"])
    def build_debug_label(self, extra):
        return f"{self.title} ({self.counter}, extra={extra})"
 
 
instance = DemoProxy.new_instance("  Example  ", 5)
java_instance = instance.java

Full Smoke Test Example

This example is designed to verify almost the entire class proxy API in one place:

  • subclass generation
  • Java field creation
  • constructor preprocessing
  • post-constructor hooks
  • overloaded method overrides
  • super() calls
  • custom Java methods
  • access to the attached Java instance

It uses java.util.ArrayList, because it is a normal subclassable Java class with overloaded methods and constructors.

from base_plugin import BasePlugin
from extera_utils.classes import (
    Base,
    java_subclass,
    jfield,
    joverride,
    joverload,
    jmethod,
    jconstructor,
    jpreconstructor,
)
from java.util import ArrayList
 
 
@java_subclass(ArrayList)
class DebugList(Base):
    add_calls = jfield("int", default=0)
    remove_calls = jfield("int", default=0)
    last_action = jfield("java.lang.String", default="created")
 
    @jpreconstructor(["int"])
    def normalize_capacity(cls, capacity):
        # Ensure capacity is never negative before Java constructor runs.
        return [max(0, capacity)]
 
    def __init__(self, *args):
        # Python-only state. This is not a Java field.
        self.history = []
 
    @jconstructor(["int"])
    def init_with_capacity(self, capacity):
        self.last_action = f"capacity:{capacity}"
        self.history.append(f"ctor(capacity={capacity})")
 
    @joverride()
    def size(self):
        # Example of overriding a normal method and still delegating to parent.
        current = super().size()
        self.history.append(f"size() -> {current}")
        return current
 
    @joverload("add", ["java.lang.Object"])
    def add_item(self, value):
        self.add_calls += 1
        self.last_action = f"add:{value}"
        self.history.append(f"add(value={value})")
        return super().add_item(value)
 
    @joverload("add", ["int", "java.lang.Object"])
    def add_at_index(self, index, value):
        self.add_calls += 1
        self.last_action = f"add_at:{index}:{value}"
        self.history.append(f"add(index={index}, value={value})")
        return super().add_at_index(index, value)
 
    @joverload("remove", ["int"])
    def remove_at_index(self, index):
        self.remove_calls += 1
        self.last_action = f"remove_at:{index}"
        self.history.append(f"remove(index={index})")
        return super().remove_at_index(index)
 
    @joverload("remove", ["java.lang.Object"])
    def remove_item(self, value):
        self.remove_calls += 1
        self.last_action = f"remove:{value}"
        self.history.append(f"remove(value={value})")
        return super().remove_item(value)
 
    @jmethod("debugSummary", "java.lang.String", [])
    def debug_summary(self):
        return (
            f"size={self.size()}, "
            f"add_calls={self.add_calls}, "
            f"remove_calls={self.remove_calls}, "
            f"last_action={self.last_action}"
        )
 
    @jmethod("historyDump", "java.lang.String", [])
    def history_dump(self):
        return " | ".join(self.history)
 
 
class ProxyDebugPlugin(BasePlugin):
    def on_plugin_load(self):
        debug_list = DebugList.new_instance(4)
 
        debug_list.add("one")
        debug_list.add("two")
        debug_list.add(1, "inserted")
        debug_list.remove("two")
 
        self.log(f"Class name: {debug_list.getClass().getName()}")
        self.log(f"Java size(): {debug_list.size()}")
        self.log(f"Python field add_calls: {debug_list.add_calls}")
        self.log(f"Python field remove_calls: {debug_list.remove_calls}")
        self.log(f"Java method debugSummary(): {debug_list.debugSummary()}")
        self.log(f"Java method historyDump(): {debug_list.historyDump()}")
 
        # Access raw Java instance from the Python peer if needed.
        self.log(f"Same object hash: {debug_list.java.hashCode()}")

What to expect

If everything works, this example should confirm that:

  • DebugList.new_instance(4) selects the ArrayList(int) constructor
  • DebugList.new_instance(4) returns the Python peer object, and debug_list.java is the raw Java instance
  • @jpreconstructor(["int"]) runs before Java construction
  • @jconstructor(["int"]) runs after Java construction
  • add_calls, remove_calls, and last_action behave like Java-backed fields
  • the correct overloaded add and remove handlers are called
  • super() reaches the original ArrayList implementation
  • debugSummary() and historyDump() exist as real Java methods on the generated class

Custom Settings Example

The settings SDK uses this system internally for custom setting factories.

Here is a simplified pattern:

from client_utils import get_media_data_controller
from base_plugin import BasePlugin
from ui.settings import Text, Custom
from extera_utils.classes import Base, java_subclass, joverride
 
from android.util import TypedValue
from android.view import View, Gravity
from android.widget import LinearLayout, FrameLayout, TextView
from android.content import Context
 
from org.telegram.messenger import AndroidUtilities
from org.telegram.ui.ActionBar import Theme
from org.telegram.ui.Components import LayoutHelper, BackupImageView, UItem
 
from com.exteragram.messenger.plugins import PluginsController, Plugin
from com.exteragram.messenger.plugins.models import CustomSetting
 
MATCH_PARENT = -1
WRAP_CONTENT = -2
 
@java_subclass(FrameLayout)
class PluginCell(Base):
    def on_post_init(self, context: Context):
        self.need_divider = False
        self.setWillNotDraw(False)
 
        self.icon_view = BackupImageView(context)
        self.icon_view.setRoundRadius(AndroidUtilities.dp(8))
        self.addView(self.icon_view, LayoutHelper.createFrame(48, 48, Gravity.LEFT | Gravity.CENTER_VERTICAL, 16, 0, 0, 0))
 
        text_layout = LinearLayout(context)
        text_layout.setOrientation(LinearLayout.VERTICAL)
 
        self.name_text_view = TextView(context)
        self.name_text_view.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText))
        self.name_text_view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16)
        self.name_text_view.setLines(1)
        self.name_text_view.setSingleLine(True)
 
        self.author_text_view = TextView(context)
        self.author_text_view.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText))
        self.author_text_view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13)
        self.author_text_view.setLines(1)
        self.author_text_view.setSingleLine(True)
 
        text_layout.addView(self.name_text_view, LayoutHelper.createLinear(MATCH_PARENT, WRAP_CONTENT))
        text_layout.addView(self.author_text_view, LayoutHelper.createLinear(MATCH_PARENT, WRAP_CONTENT, 0, 2, 0, 0))
        self.addView(text_layout, LayoutHelper.createFrame(MATCH_PARENT, WRAP_CONTENT, Gravity.LEFT | Gravity.CENTER_VERTICAL, 76, 0, 16, 0))
 
    def set_data(self, plugin: Plugin, divider: bool):
        self.need_divider = divider
        self.name_text_view.setText(plugin.getName() or "Unknown Plugin")
 
        version = plugin.getVersion() or "1.0"
        author = plugin.getAuthor() or "Unknown"
        self.author_text_view.setText(f"v{version}{author}")
 
        show_icon = plugin.getPack() is not None and plugin.getIndex() >= 0
        pack, index = (plugin.getPack(), plugin.getIndex()) if show_icon else ("exteraPlugins", 0)
        get_media_data_controller().setPlaceholderImageByIndex(self.icon_view, pack, index, "100_100")
 
        self.requestLayout()
    
    @joverride()
    def onMeasure(self, widthMeasureSpec: int, heightMeasureSpec: int):
        super().onMeasure(
            View.MeasureSpec.makeMeasureSpec(View.MeasureSpec.getSize(widthMeasureSpec), View.MeasureSpec.EXACTLY),
            View.MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(68), View.MeasureSpec.EXACTLY),
        )
 
 
@java_subclass(CustomSetting.Factory)
class PluginCellSettingFactory(Base):
    @joverride()
    def create(self, plugin: Plugin, setting: CustomSetting, args):
        if not args:
            return None
        
        item = UItem.ofFactory(self.java_class())
        item.object = args
        return item
 
    @joverride()
    def createView(self, context, list_view, current_account, class_guid, resources_provider):
        view = PluginCell.new_instance(context)
        return view.java
 
    @joverride()
    def bindView(self, view, item, divider, adapter, list_view):
        if not item.object:
            return False
 
        PluginCell.from_java(view).set_data(item.object, divider)
 
 
class TestPlugin(BasePlugin):
    def on_plugin_load(self):
        # plugin_cell_setting_factory_instance
        self.pcsfi = PluginCellSettingFactory.new_java_instance()
        UItem.UItemFactory.setup(self.pcsfi)
 
    def create_settings(self):
        plugins = PluginsController.getInstance().plugins.values().toArray()
        if not plugins:
            return [
                Text("Restart plugin please, no plugins found")
            ]
 
        return [
            Custom(
                factory=self.pcsfi,
                factory_args=_p
            ) for _p in plugins
        ]

If you instantiate this proxy with FactoryProxy.new_instance(), the result is the Python peer. Pass factory.java into APIs which require CustomSetting.Factory.

For a higher-level settings guide, see Plugin Settings.

Advanced Notes

Method Resolution Order

For managed overrides, the proxy system installs a Python bridge so that super() calls the parent Java implementation.

Because of this, it is best to use super() only for methods marked with @joverride(...) or @joverload(...).

Exact Signature Dispatch

When Java calls Python, the proxy system uses a full signature key internally:

methodName:int:java.lang.String

This is what makes overloaded methods and constructors work correctly.

Primitive Types

Supported primitive type names:

  • void
  • boolean
  • byte
  • char
  • short
  • int
  • long
  • float
  • double

When To Use It

Use class proxy when:

  • a Java API requires subclassing instead of interface proxying
  • you need parent-method calls with super()
  • you need overload-specific behavior
  • you want Java fields or extra Java methods on the generated object

Do not use it when a simple dynamic_proxy(...) is enough for an interface.

Also make sure the base Java class is actually subclassable. Final classes cannot be used as a proxy base class.

Troubleshooting

super() does not work

Check that:

  • the method is declared with @joverride(...) or @joverload(...)
  • the method name or signature matches the Java parent method

My overload is not called

Check that:

  • the Java name is correct
  • the argument types match the real Java signature exactly
  • primitive types use primitive names such as "int" instead of wrapper classes

Constructor hook is not called

Check that:

  • @jpreconstructor(...) or @jconstructor(...) uses the correct constructor signature
  • for wildcard constructor hooks, your annotations are either complete or you intentionally omitted the signature

Field value is always None

Check that:

  • the field was declared with jfield(...)
  • the field type is valid
  • the default value is compatible with the declared Java type

Class generation fails immediately

Check that:

  • the base class is not final
  • constructor signatures match a real constructor
  • overload signatures match real Java method argument types