Skip to content

Annotation Processor

A notable piece of trivia about this library is that, at first, JSignal’s standard component library was written in Kotlin. The reasons for switching to Java are outside the scope of this article, but one of the most attractive features of Kotlin, from the standpoint of component design, is it’s support for named arguments because it largley replaces the boilerplate builder pattern.

While JSignal is designed to be entierly usable without any compile time processing, writing builders for components can be quite tedious and also error prone for those unfamiliar with the workings of it’s reactive system. For those reasons, the library provides a fairly simple annotation processor for generating component builders.

Usage

A typical component signature in JSignal looks like this:

public class Button extends Component ...

To automatically create a builder, components must be annotated with @GeneratePropComponent and instead of extending Component will extend a class called <component name>PropComponent (this is the name of the class generated by the annotation processor). This new parent class will exist on the classpath in the same package as the component, so it is important to ensure that the package does not already contain a class with that name. Below is an example for our imaginary button component:

@GeneratePropComponent
public class Button extends ButtonPropComponent ...

The ButtonPropComponent will contain a static inner class called Builder with setters for each of the component’s properties. It will also contain a static builder method to instantiate the builder, and an empty protected onBuild method, meant to be overridden, that is called after all the properties have been set.

Properties

When generating the builder, the annotation processor scans the component class for fields annotated with @Prop. These fields must either be public or package protected, and in order for them to be reactive (though they aren’t required to be) they must use the Supplier type. Let’s add some properties to our button example:

@GeneratePropComponent
public class Button extends ButtonPropComponent {
@Prop
Supplier<String> text = Constant.of("Default Button Text");
@Prop
Supplier<Integer> color = Constant.of(0xFFFFFF); // default color
@Prop
Supplier<Icon> icon; // null indicates no icon, so no default value needed
...
}

Now the button component can be instantiated like so:

var buttonText = Signal.create("Initial Text")
var showGreen = Signal.create(true);
var button = Button.builder()
.text(buttonText) // reactive signal value
.color(() -> showGreen.get() ? 0x00FF00 : 0x0000FF) // reactive computed value
.icon(new Icon("resource/location.png")) // constant value
.build();

By default, properties are considered optional and do not need to be set before calling build, but required properties are also supported using @Prop(required = true). The processor generates fluent builder interfaces for ensuring each required property gets set, using the order they are declared in.