The component binding framework wires a model field to the matching
component in a Form or Container by name. The Maven plugin generates
a <SimpleName>Cn1Binder per @Bindable class at build time, so the
wiring happens through direct symbol references — no reflection, no
listener bookkeeping in application code.
It’s a thin alternative to the imperative UiBinding API
(com.codename1.properties.UiBinding). Both can be used together.
Annotate the model
@Bindable
public class LoginModel {
@Bind(name = "userField", attr = BindAttr.TEXT)
@Required
private String user;
public String getUser() { return user; }
public void setUser(String u) { this.user = u; } // (1)
@Bind(name = "rememberMe", attr = BindAttr.SELECTED)
public boolean remember; // (2)
@Bind(name = "banner", attr = BindAttr.UIID, twoWay = false)
public String bannerStyle;
@Bind(name = "fullName",
attr = BindAttr.TEXT,
getter = "computeFullName",
setter = "applyFullName") // (3)
private String fullName;
// both run during the initial bind, against a model that may still be
// empty, so neither can assume a value is present
public String computeFullName() { return fullName == null ? "" : fullName.toUpperCase(); }
public void applyFullName(String f){ this.fullName = f == null ? null : f.trim(); }
}JavaBeans
getUser/setUserare detected automatically when the field is private. The processor instrumentssetUserto fire a change notification for two-way bindings.Public field still works — direct field access falls back when no accessor matches the JavaBeans convention.
Explicit
getter/setteroverride the JavaBeans search. Use when the convention doesn’t apply (renamed accessors, transforms, fluent setters).
Every @Bind(name=…) is looked up against Component#getName(),
walking the container’s children recursively until a match is found.
The default lookup matches GUI-builder names exactly.
Attribute kinds
BindAttr | Mirrors |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
| Calls |
|
|
Bind at runtime
Binding is the handle the binder returns:
| Method | Purpose |
|---|---|
| Push the current model values into every bound
component. Use after mutating the model outside the
form, or to re-sync after |
| Pull current component values back into the model. Useful before validating / submitting when none of the bindings is two-way. |
| Remove every listener the binder installed and unregister the binding from the change-notification fan-out. |
Two-way bindings and the change-notification contract
A binding declared twoWay = true flows in both directions:
Component → model. The generated binder installs a listener on the editable component (
TextField,CheckBox, etc.). When the user mutates the component, the binder calls the model’s setter (or assigns the public field). The setter’s instrumented exit callsBinders.notifyChanged(this).Model → component. Application code that mutates the model through a setter causes
Binders.notifyChanged(this)to fire, which walks every live binding for that model and pushes the new value into the matching component.
The two paths together would loop forever if left alone — the model
setter fires a change event, which refreshes the component, which
fires its own change event, which calls the model setter again, …
To break the loop, every framework-initiated mutation runs inside an
update region guarded by a thread-local depth counter. While the
counter is positive, notifyChanged is a no-op and component
listeners short-circuit.
Concretely:
Binders.bind(model, container)enters an update region for the initial model → component push.Binding#refresh()enters an update region for every subsequent model → component push.Binding#commit()enters an update region for the component → model pull.The generated component listener enters an update region before calling the setter.
Setter instrumentation
For every two-way @Bind field whose write accessor is a method
(whether you wrote @Bind(setter="setName") or the processor detected
setName(String) via the JavaBeans convention), the
cn1:process-annotations Mojo reads the original .class file with
ASM and inserts the equivalent of:
public void setName(String name) {
this.name = name;
com.codename1.binding.Binders.notifyChanged(this); // injected
}
The injection lands before every return point of the setter. The
instrumented setter is written back to target/classes, replacing the
original. Application code that calls model.setName(…) — from any
thread, any code path — now triggers the binding fan-out
automatically.
Limitation — cross-field setter chains
When a setter synchronously mutates a second bound field
(setFirstName also calls setFullName, for example), whether the
second field’s notifyChanged is suppressed depends on who called the
first. An application calling setFirstName directly isn’t inside an
update region, so the nested setFullName notifies at depth zero and
its component refreshes normally. Reached from the generated component
listener or from commit(), the chain runs inside a region that’s
already open, the second field’s notifyChanged lands in it and is
suppressed, and the bound component for fullName won’t catch up
until something exits the region and a separate event drives the
refresh. If you need the cross-field update to propagate, call
binding.refresh() explicitly after the setter chain completes, or
restructure the model so the cross-field mutation happens outside the
setter’s call frame — for example via a Display.callSerially.
commit() is worse than suppression, and only one of the two remedies survives
it. When both fields are bound two-way and fullName is emitted after
firstName, the generated _commit carries on to the fullName component,
reads the value still sitting in it, and calls setFullName — overwriting what
setFirstName just derived. An immediate binding.refresh() afterward
publishes the overwritten value rather than the derived one, so that remedy
doesn’t help here.
The deferred write still does. Display.callSerially() queues the write and
returns, so _commit finishes and leaves its update region before the deferred
setFullName runs and notifies at depth zero.
Binding the derived field one way stops that write but doesn’t finish the
job: one-way fields aren’t instrumented, and the setFirstName notification is
still suppressed by the commit’s region, so the model holds the derived value
while the screen keeps the old one. If you go that way, call
binding.refresh() after the commit to publish it.
POJOs versus Property objects
A String, int, or boolean field is read and written through the
resolved accessors. A Property<T, ?> field is read through get()
and written through set() — so existing PropertyChangeListener
subscribers fire as expected. Both styles can sit on the same class.
Validation annotations
The same @Bind field can carry validation annotations that are wired
into a com.codename1.ui.validation.Validator. The validator is built
by the generated binder at the moment you call Binders.bind(…) and
is reachable via Binding#getValidator() — so the same generated
plumbing that pushes model values into components also installs the
constraints that gate them.
| Annotation | Maps to | Notes |
|---|---|---|
|
| "Field must be non-empty." |
|
| Minimum string length. |
|
| Same dialect as |
|
| The standard email regex (also accepts empty — stack with |
|
|
|
|
| Bounds are inclusive; default range is unbounded. |
|
| Whitelist of allowed string values. |
|
| Escape hatch — the class must have a public no-arg constructor and implement |
Multiple annotations on the same field are combined into a
GroupConstraint (first failure wins), matching the behaviour of
Validator.addConstraint(Component, Constraint…).
After binding, drive validation through the returned handle:
// set this first: bind() attaches the constraints, and each one picks a
// data-change or an action listener from this flag as it attaches
Validator.setValidateOnEveryKey(true);
LoginModel model = new LoginModel();
Binding b = Binders.bind(model, form);
// Auto-disable a submit button until everything is valid. Pass the
// button you built the form with: Container has no lookup by name.
b.getValidator().addSubmitButtons(submitButton);
// Programmatic gate before saving:
if (b.getValidator().isValid()) {
repository.save(model);
}
Binding#getValidator() never returns null — when the model has no
validation annotations the validator is empty and isValid() returns
true. The validator owns the listener and emblem plumbing on the
matching components; calling addSubmitButtons after bind is the
typical pattern.
Build-time validation
In addition to the runtime constraints above, the annotation processor itself fails the build when:
@Bindis applied to a field with no accessible read path — the field must be public, declare a JavaBeansgetX/isXgetter, or the annotation must name an explicitgetter.A two-way
@Bindfield on aTEXTorSELECTEDattr has no writable accessor. SettwoWay=falseto keep the binding one-way, or add a setter / explicitsetter=.@Bind(name=…)is empty.The field’s static type isn’t supported (raw collections, opaque references that aren’t
@Bindablethemselves, …).@Regexis missing a pattern, or@ExistInis missing values.
Errors are accumulated so a single build run reports every offending field at once.
How the plumbing works
cn1:process-annotations writes one
<SimpleName>Cn1Binder per @Bindable class in the source class’s
package, plus a single cn1app.BinderBootstrap whose constructor calls
UserCn1Binder.register(), LoginModelCn1Binder.register(), … for
every accepted @Bindable class. At app start:
On iOS / Android the build server probes the project zip for
cn1app/BinderBootstrap.classand splicesnew cn1app.BinderBootstrap();into the per-build application stub beforeDisplay.init. ParparVM rename and R8 obfuscation rewrite the direct symbol reference together with the generated class so the binding still resolves after the pass.On JavaSE
JavaSEPort#postInitloads the bootstrap viaClass.forName("cn1app.BinderBootstrap")— the unobfuscated classloader path.
Projects with no @Bindable classes produce no bootstrap; the build
server probe falls through and the registry stays empty.
The runtime registry is keyed on Class#getName(); obfuscation renames
the call sites and the registered keys together within a single
execution.