Pass objects through the bridge - fakeivchenko/lwjwae GitHub Wiki
A binding and an event carry text by default. With a codec, they carry records and other objects: the page passes a JavaScript object, and Java receives it as the type that it asks for, and back. In this guide, a task list keeps its tasks in Java as records.

With the lwjwae Gradle plugin, one line in build.gradle.kts adds a codec at the version that the
plugin knows:
lwjwae {
jackson() // or gson(), or jsonb()
}The application finds the codec on the classpath by itself. Without the plugin, add
dev.ivchenko.lwjwae.codec:lwjwae-codec-jackson to runtimeOnly, or pass a codec to
ApplicationParameters.builder().codec(...).
-
Describe what crosses the bridge as a record:
public record Task(String title, boolean done) {}
-
Bind a function that takes a
Taskand returns the list, and listen to an event that carries a number:public class TypedBridge { private static final List<Task> TASKS = new CopyOnWriteArrayList<>(); public static void main(String[] args) { Application.launch( WindowParameters.of("Tasks", 560, 440), window -> { window.bind("addTask", Task.class, TypedBridge::add); window.listen("toggle", Integer.class, index -> TypedBridge.toggle(window, index)); window.loadResource("typed/index.html"); }); } private static List<Task> add(Task task) { TASKS.add(task); return TASKS; } private static void toggle(Window window, int index) { Task task = TASKS.get(index); TASKS.set(index, new Task(task.title(), !task.done())); window.emit("tasks", TASKS); } }
bind(name, Class, handler)decodes the argument into the class, and encodes what the handler returns.listen(name, Class, listener)decodes the payload of an event.emit(name, Object)encodes an object into the payload. -
On the page, pass objects and receive them:
const list = document.querySelector("#tasks"); function show(tasks) { list.replaceChildren(...tasks.map((task, index) => { const item = document.createElement("li"); const box = Object.assign(document.createElement("input"), { type: "checkbox", checked: task.done }); box.addEventListener("change", () => lwjwae.emit("toggle", index)); const title = Object.assign(document.createElement("span"), { textContent: task.title }); title.classList.toggle("done", task.done); item.append(box, title); return item; })); } document.querySelector("#add").addEventListener("submit", async (event) => { event.preventDefault(); const title = document.querySelector("#title"); show(await window.addTask({ title: title.value, done: false })); title.value = ""; }); lwjwae.listen("tasks", (event) => show(event.payload));
window.addTaskresolves to the list as an array of objects, andevent.payloadof an event that Java emitted with an object is the object, not its text.
A codec reads and creates the records through reflection, which a native image allows only for
the classes that its metadata names. Mark the outermost type that crosses the bridge with
@BridgeType:
@BridgeType
public record Task(String title, boolean done) {}The lwjwae-processor annotation processor writes the metadata for it and for every type that it
holds: the components of a record, the fields of a class, the type arguments of a List<Task>.
The Gradle plugin adds the processor from lwjwae 0.8.0, so there is nothing else
to do.
Without the plugin, or with an older lwjwae, add the processor to annotationProcessor yourself, or
list every type that crosses the bridge, and the types that they hold, in
src/main/resources/META-INF/native-image/GROUP/NAME/reachability-metadata.json:
{
"reflection": [
{
"type": "com.example.guides.Task",
"allDeclaredConstructors": true,
"allDeclaredMethods": true,
"allDeclaredFields": true
}
]
}- A handler runs on a virtual thread, so it may block, as
addwould to write a file. - A binding on the
Applicationinstead of aWindowreaches every window, and its handler can take the window that called; see Open several windows. - Without a codec, the typed methods fail with a message that says so, and the text methods keep working.