Using Java and Kotlin

Kanzi Java API implements platform-independent Java proxies for Kanzi classes, providing rich access to the Kanzi functionality in Java and Kotlin.

This enables you to create applications that are tightly integrated with the Android APIs. On Android, you can use the Kanzi Java API directly from the Android UI thread without the need for dispatcher mechanisms. See Developing with the Kanzi Android framework (droidfw).

This topic provides an overview of the fundamentals of Kanzi Java API. For complete API reference, see Kanzi Java API reference.

Requirements

Kanzi Java API requires Java 8 (1.8) or higher.

Using properties

Kanzi Java API classes contain static members for built-in property types such as Node.OpacityProperty. To read and write these properties:

// Set property.
node.setProperty(Node.OpacityProperty, 0.5f);

// Get property.
float opacity = node.getProperty(Node.OpacityProperty);
// Set property.
node.setProperty(Node.OpacityProperty, 0.5f)

// Get property.
val opacity = node.getProperty(Node.OpacityProperty)

To access a property by name, create an instance of DynamicPropertyType or AbstractPropertyType with a full name of the property type:

// Use DynamicPropertyType to access a property type when the type is known.
DynamicPropertyType<Boolean> dynamicProperty =
    new DynamicPropertyType<>("Node.Visible", Boolean.class);
node.setProperty(dynamicProperty, false);

// Use AbstractPropertyType to access a property type when the type is not known.
AbstractPropertyType abstractProperty = new AbstractPropertyType("Node.Visible");
node.setProperty(abstractProperty, false);
// Use DynamicPropertyType to access a property type when the type is known.
val dynamicProperty = DynamicPropertyType("Node.Visible", Boolean::class.java)
node.setProperty(dynamicProperty, false)

// Use AbstractPropertyType to access a property type when the type is not known.
val abstractProperty = AbstractPropertyType("Node.Visible")
node.setProperty(abstractProperty, false)

To define a new property type, create a public static member instance of PropertyType in your class, and annotate with @Metadata:

@Metadata
@EditorInfo(displayName = "Custom Property")
public static final PropertyType<String> CustomProperty =
    new PropertyType<>("MyNode2D.Custom", String.class);

// Properties can also have an automatically generated name.
// In this case 'MyNode2D.Other'.
@Metadata
public static final PropertyType<String> OtherProperty = new PropertyType<>(String.class);
// You must place this inside the "companion object".

@Metadata
@EditorInfo(displayName = "Custom Property")
val CustomProperty = PropertyType("MyNode2D.Custom", String::class.java)

// Properties can also have an automatically generated name.
// In this case 'MyNode2D.Other'.
@Metadata
val OtherProperty = PropertyType(String::class.java)

To learn more about the metadata that you can set for your property type, see Reference for showing Kanzi Engine plugin custom types in Kanzi Studio.

See Property system.

Using messages

Kanzi Java API classes contain static members for built-in message types, such as ButtonConceptMetadata.ToggleStateChangedMessageArguments.

To dispatch a message:

// Construct message arguments.
ToggleButton2D.ToggleStateChangedMessageArguments args =
    new ToggleButton2D.ToggleStateChangedMessageArguments();

// Set message arguments.
args.setToggleStateProperty(1);

// Dispatch message.
node.dispatchMessage(Button2D.ToggleStateMessage, args);
// Construct message arguments.
val args = ButtonConceptMetadata.ToggleStateChangedMessageArguments()

// Set message arguments.
args.toggleStateProperty = 1

// Dispatch message.
node.dispatchMessage(Button2D.ToggleStateMessage, args)

To react when a node receives a message, add a message handler to the node:

// Add message subscription.
button.addMessageHandler(Button2D.ToggleStateMessage, messageArguments -> {
    // Extract arguments through accessors..
    Integer toggleState = messageArguments.getToggleStateProperty();
    // .. or through generic accessors
    toggleState = messageArguments.getArgument(
        ButtonConceptMetadata.ToggleStateChangedMessageArguments.ToggleStateProperty);
});
// Add message subscription.
button.addMessageHandler(Button2D.ToggleStateMessage)
{ messageArguments ->
    // Extract arguments.
    val toggleState = messageArguments.toggleStateProperty
}

To use a message by name, create an instance of AbstractMessageType with the full name of the message type:

// Get reference to a message type.
AbstractMessageType messageType = new AbstractMessageType("Message.MyMessage");
DynamicPropertyType<Boolean> argumentProperty =
    new DynamicPropertyType<>("MyMessage.Argument", Boolean.class);

// Construct message arguments.
MessageArguments args = new MessageArguments();

// Set message arguments.
args.setArgument(argumentProperty, false);

// Send the message.
node.dispatchMessage(messageType, args);
// Get reference to a message type.
val messageType = AbstractMessageType("Message.MyMessage")
val argumentProperty = DynamicPropertyType("MyMessage.Argument", Boolean::class.java)

// Construct message arguments.
val args = MessageArguments()

// Set message arguments.
args.setArgument(argumentProperty, false)

// Send the message.
node.dispatchMessage(messageType, args)

To define a new message type:

  1. (Optional) Define a message argument class that holds the argument property types of your message.

    The message argument class that you define must derive from the base class MessageArguments, or another message argument class.

    You do not have to define a message argument class. Instead, you can use an existing message argument class, including the base class MessageArguments.

    @Metadata
    public static class ExampleMessageArguments extends MessageArguments
    {
        // Define metaclass.
        @Metadata
        public static Metaclass metaclass =
            new Metaclass("Example.MyNode2D.ExampleMessageArguments");
    
        // Define properties.
        @Metadata
        public static PropertyType<Integer> SomeProperty = new PropertyType<>(Integer.class);
    
        // Optionally, define property accessors. You can instead use the generic argument
        // accessors.
        public Integer getSomeProperty()
        {
            return getArgument(SomeProperty);
        }
        public void setSomeProperty(Integer value)
        {
            setArgument(SomeProperty, value);
        }
    
        // Define constructors.
        // A constructor with a long argument, which Kanzi uses to wrap native arguments.
        public ExampleMessageArguments(long nativeObject)
        {
            super(nativeObject);
        }
        // Any additional constructors for you to use when dispatching messages.
        public ExampleMessageArguments()
        {
            super();
        }
    }
    
    @Metadata
    class ExampleMessageArguments : MessageArguments {
        companion object {
            // Define metaclass.
            @Metadata
            var metaclass = Metaclass("Example.MyNode2D.ExampleMessageArguments")
    
            // Define properties.
            @Metadata
            val SomeProperty = PropertyType(Int::class.java)
        }
    
        // Optionally, define property accessors. You can instead use the generic argument accessors.
    
        fun getSomeProperty(): Int? {
            return getArgument(SomeProperty)?.toInt()
        }
    
        fun setSomeProperty(value: Int) {
            setArgument(SomeProperty, value)
        }
    
        // Define constructors.
    
        // A constructor with a long argument, which Kanzi uses to wrap native arguments.
        constructor(nativeObject: Long) : super(nativeObject)
    
        // Any additional constructors for you to use when dispatching messages.
        constructor() : super()
    }
    
  2. Create a public static member instance of MessageType in your class, and annotate with @Metadata:

    @Metadata
    public static final MessageType<ExampleMessageArguments> ExampleMessage =
        new MessageType<>("Message.MyNode2D.Example", MessageRouting.MessageRoutingBubbling,
            ExampleMessageArguments.class);
    
    // You must place this inside the "companion object".
    
    @Metadata
    val ExampleMessage = MessageType(
        "Message.MyNode2D.Example",
        MessageRouting.MessageRoutingBubbling,
        ExampleMessageArguments::class.java
    )
    

To learn more about the metadata that you can set for your message and message argument type, see Reference for showing Kanzi Engine plugin custom types in Kanzi Studio.

See Using messages.

Creating instances

Concrete Kanzi Java API types come with create methods that you can use to create instances.

The create methods of object types, classes derived from KanziObject, return an ObjectRef instance that is an owning reference to the new object.

Managing instance lifetime

The ObjectRef type enables shared ownership of an instance of KanziObject derived type between the native backend and Java ObjectRef instances. When all ObjectRef instances are closed and no native references exist, the object is reclaimed. If application code attempts to access the object in this state, Kanzi throws a StaleObjectException. To prevent this, maintain an ObjectRef instance for the objects that you use.

Although ObjectRef instances are automatically garbage collected, to have predictable memory consumption, you can manually close such instances.

// Create an EmptyNode2D node.
ObjectRef<EmptyNode2D> nodeRef = EmptyNode2D.create(domain, "My Empty Node");

// Access the EmptyNode2D instance from within the ObjectRef.
EmptyNode2D node = nodeRef.get();

// Use the node.

// Close the ObjectRef.
nodeRef.close();

// If you try to access the node variable after closing its ObjectRef,
// Kanzi throws a StaleObjectException.
node.setName("New Name");
// Create an EmptyNode2D node.
val nodeRef = EmptyNode2D.create(domain, "My Empty Node")

// Access the EmptyNode2D instance from within the ObjectRef.
val node = nodeRef.get()

// Use the node.

// Close the ObjectRef.
nodeRef.close()

// If you try to access the node variable after closing its ObjectRef,
// Kanzi throws a StaleObjectException.
node.name = "New Name"

You can also associate the lifetime of an ObjectRef instance to a scope using the Java try-with-resources statement or the Kotlin use function:

// Create an EmptyNode2D node.
try (ObjectRef<EmptyNode2D> nodeRef = EmptyNode2D.create(domain, "My Empty Node"))
{
    // Access the EmptyNode2D instance from within the ObjectRef.
    EmptyNode2D node = nodeRef.get();

    // Use the node.

    // Kanzi automatically closes the nodeRef at the end of the try block scope.
}
EmptyNode2D.create(domain, "My Empty Node").use { nodeRef ->
    // Access the EmptyNode2D instance from within the ObjectRef.
    val node = nodeRef.get()

    // Use the node.

    // Kanzi automatically closes the nodeRef at the end of the use block scope.
}

You can also create an owning ObjectRef instance from a non-owning reference.

// Access an EmptyNode2D instance.
EmptyNode2D node = getNode(domain);

// Construct a Reference to the existing object.
// If the node is already stale, throws a StaleObjectException.
ObjectRef<EmptyNode2D> ref1 = new ObjectRef<>(node);

// Try to construct a Reference to the existing object.
// If the node is already stale, returns null.
ObjectRef<EmptyNode2D> ref2 = node.tryCreateObjectRef(EmptyNode2D.class);
// Access an EmptyNode2D instance.
val node = getNode(domain)

// Construct a Reference to the existing object.
// If the node is already stale, throws a StaleObjectException.
val ref1 = ObjectRef(node)

// Try to construct a Reference to the existing object.
// If the node is already stale, returns null.
val ref2 = node.tryCreateObjectRef<EmptyNode2D>(EmptyNode2D::class.java)

Multiple owning references

When ObjectRef instances are passed between blocks of code, it can become unclear when to close an instance. Because Java passes instances by reference, what appears to be independent instances, could all refer to the same strong reference. To avoid these issues, Kanzi uses the following guidelines with ObjectRef instances and recommends the users to do the same:

  • When returning an ObjectRef instance as a return value from a method, the ownership is passed with the instance. Methods must either return a clone of the instance, or an unclosed local-only reference. This also means that ObjectRef instances that are received from a method return, are owned by the calling code and can be closed when no longer needed or left for garbage collection.

    // When returning a member ObjectRef, return a clone.
    ObjectRef<EmptyNode2D> getNode1()
    {
        return mNode.clone();
    }
    
    // You can return a local ObjectRef directly, but do not close it explicitly.
    ObjectRef<EmptyNode2D> getNode2()
    {
        ObjectRef<EmptyNode2D> localRef = EmptyNode2D.create(getDomain(), "My Empty Node");
        return localRef;
    
        // Here, do not close the localRef.
    }
    
    // When returning a member ObjectRef, return a clone.
    fun getNode1(): ObjectRef<EmptyNode2D> {
        return mNode.clone()
    }
    
    // You can return a local ObjectRef directly, but do not close it explicitly.
    fun getNode2(): ObjectRef<EmptyNode2D> {
        var localRef = EmptyNode2D.create(domain, "My Empty Node")
    
        return localRef;
    
        // Here, do not close the localRef.
    }
    
  • When passing an ObjectRef instance as an argument to a method, the ownership of that instance is retained with the caller. When implementing a method that takes an ObjectRef as an argument, the ownership is temporary. If the method stores the instance in a member variable, or otherwise wants to extend the lifetime of the object beyond the scope of the method, it must clone the ObjectRef instance.

    void myFunction(ObjectRef<Node2D> node)
    {
        // You can use the node as-is, except for closing or storing the node.
        // If you need to store the reference, make a clone.
        mNode = node.clone();
    }
    
    fun myFunction(node: ObjectRef<Node2D?>) {
        // You can use the node as-is, except for closing or storing the node.
        // If you need to store the reference, make a clone.
        mNode = node.clone()
    }
    

Creating a custom type

You can use Java or Kotlin to extend the functionality of Kanzi Engine by creating a custom type. You can either derive from one of the extensible Kanzi types or you can define an abstract type.

For example, to create a custom 2D node:

  1. Derive a class from Node2D.

    static class MyNode2D extends Node2D
    {
        private MyNode2D(Domain domain, long nativeNode, Metaclass metaclass)
        {
            super(domain, nativeNode, metaclass);
        }
    
    class MyNode2D private constructor(domain: Domain, nativeNode: Long, metaclass: Metaclass) :
        Node2D(domain, nativeNode, metaclass) {
    
        companion object {
    
  2. Kanzi requires these items for each custom node:

    • Metaclass. The metaclass must be present in this form:

      @Metadata
      public static final Metaclass metaclass = new Metaclass("Example.MyNode2D");
      
      // You must place this inside the "companion object".
      
      @Metadata
      val metaclass = Metaclass("Example.MyNode2D")
      

      You can provide additional metadata with the @EditorInfo annotation. See Reference for showing Kanzi Engine plugin custom types in Kanzi Studio.

    • Factory. Derived types must provide a static public create method with the specific arguments. The implementation of the create method is usually a call to the createDerived method on the type that you are extending.

      static public ObjectRef<MyNode2D> create(Domain domain, String name)
      {
          return Node2D.createDerived(domain, name, metaclass);
      }
      
      // You must place this inside the "companion object".
      
      @JvmStatic
      fun create(domain: Domain, name: String): ObjectRef<MyNode2D> {
          return Node2D.createDerived(domain, name, metaclass)
      }
      
    • (Optional) Custom initialization. During the constructor Kanzi does not fully initialize custom Kanzi objects. Place custom initialization code in the initialize() method.

      @Override
      protected void initialize()
      {
          super.initialize();
      
          // Add custom initialization code after calling super.initialize().
      }
      
      override fun initialize() {
          super.initialize()
      
          // Add custom initialization code after calling super.initialize().
      }
      
  3. (Optional) Create custom property types.

  4. (Optional) Create custom message arguments, types, and handlers.

  5. (Optional) Define custom node behavior by overriding relevant methods.

    @Override
    protected void arrangeOverride(Vector2 actualSize)
    {
        super.arrangeOverride(actualSize);
    
        // Define custom behavior.
    }
    
    override fun arrangeOverride(actualSize: Vector2?) {
        super.arrangeOverride(actualSize)
        // Define custom behavior.
    }
    

Implementing custom resource loading

To implement custom resource loading in Java or Kotlin, define a custom resource manager protocol.

ResourceManager resourceManager = domain.getResourceManager();
resourceManager.registerProtocolHandler(
    "MyProtocol", new ResourceManager.ProtocolHandler() {
        @Override
        public Result handle(ResourceManager resourceManager, String url, String protocol,
            String hostname, String path)
        {
            if (shouldLoadSynchronously)
            {
                // Load a resource synchronously.
                ObjectRef<Resource> resource = loadSynchronousResource();
                return new Result(resource);
            }
            else
            {
                // Use load task for asynchronous loading.
                return new Result(new ResourceManager.LoadTask() {
                    ObjectRef<Resource> mResource = null;
                    @Override
                    public void loadFunction()
                    {
                        // Perform thread-independent loading.
                    }

                    @Override
                    public void finishFunction()
                    {
                        // Create and store the resource.
                        mResource = createResource();
                    }

                    @Override
                    public ObjectRef<? extends Resource> getResult()
                    {
                        // Return the created resource.
                        return mResource.clone();
                    }

                    @Override
                    public void close()
                    {
                        mResource.close();
                    }
                });
            }
        }
    });
domain.resourceManager.registerProtocolHandler("MyProtocol")
{ resourceManager, url, protocol, hostname, path ->
    if (shouldLoadSynchronously) {
        // Load a resource synchronously.
        val resource = loadSynchronousResource()
        ProtocolHandler.Result(resource)
    } else {
        // Use load task for asynchronous loading.
        ProtocolHandler.Result(object : ResourceManager.LoadTask() {
            var mResource: ObjectRef<Resource>? = null
            override fun loadFunction() {
                // Perform thread-independent loading.
            }

            override fun finishFunction() {
                // Create and store the resource.
                mResource = createResource()
            }

            override fun getResult(): ObjectRef<out Resource>? {
                // Return the created resource.
                return mResource!!.clone()
            }

            override fun close() {
                mResource!!.close()
            }
        })
    }
}

In case of failure, the resource protocol handle can throw an exception, which the Kanzi Resource Manager handles.

By default the LoadTask calls both the loadFunction and the finishFunction. To set the LoadTask to call only finishFunction, pass ResourceLoadTaskType.FinishOnly to the constructor.

Resource protocols can support the reload operation by implementing the ReloadProtocolHandler. Kanzi calls the reload handler to reinitialize and existing resource when the OpenGL context is invalidated.

ResourceManager resourceManager = domain.getResourceManager();
resourceManager.registerProtocolHandler("MyReloadProtocol",
    new ResourceManager.ProtocolHandler() {
        @Override
        public Result handle(ResourceManager resourceManager, String url, String protocol,
            String hostname, String path)
        {
            TextureBuilder builder = new TextureBuilder()
                                         .setName("Test Empty Texture")
                                         .setWidth(128)
                                         .setHeight(128);

            ObjectRef<Texture> texRef = builder.build(getDomain());
            return new Result(texRef);
        }
    },
    new ResourceManager.ReloadProtocolHandler() {
        @Override
        public void handle(ResourceManager resourceManager, String url, String protocol,
            String hostname, String path, Resource resource)
        {
            if (resource instanceof Texture)
            {
                TextureBuilder builder = new TextureBuilder()
                                             .setName("Reloaded Empty Texture")
                                             .setWidth(128)
                                             .setHeight(128);
                builder.recreate((Texture) resource);
            }
        }
    });
domain.resourceManager.registerProtocolHandler("MyReloadProtocol",
    { resourceManager, url, protocol, hostname, path ->
        val builder = TextureBuilder()
            .setName("Test Empty Texture")
            .setWidth(128)
            .setHeight(128)
        val texRef = builder.build(getDomain())
        ProtocolHandler.Result(texRef)
    },
    { resourceManager, url, protocol, hostname, path, resource ->
        if (resource is Texture) {
            val builder = TextureBuilder()
                .setName("Reloaded Empty Texture")
                .setWidth(128)
                .setHeight(128)
            builder.recreate(resource)
        }
    })

Creating a data source

A data source enables you to access third-party data from your Kanzi application.

To create a data source:

  1. Create the DataSource class.

    class MyDataSource extends DataSource
    {
        @Metadata
        public static final Metaclass metaclass = new Metaclass("MyPlugin.MyDataSource");
    
        public static ObjectRef<MyDataSource> create(Domain domain, String name)
        {
            return DataSource.createDerived(domain, name, MyDataSource.metaclass);
        }
    
        private MyDataSource(Domain domain, long nativeHandle, Metaclass metaclass)
        {
            super(domain, nativeHandle, metaclass);
        }
    
        @Override
        protected void initialize()
        {
            super.initialize();
    
            createData();
        }
    
        private void createData() {}
    }
    
    class MyKotlinDataSource private constructor(
        domain: Domain,
        nativeHandle: Long,
        metaclass: Metaclass
    ) :
        DataSource(domain, nativeHandle, metaclass) {
        override fun initialize() {
            super.initialize()
            createData()
        }
    
        private fun createData() {}
    
        companion object {
            @Metadata
            val metaclass = Metaclass("MyPlugin.MyDataSource")
    
            @JvmStatic
            fun create(domain: Domain?, name: String?): ObjectRef<MyKotlinDataSource> {
                return createDerived(domain, name, metaclass)
            }
        }
    }
    
  2. Add the required members, accessors, and the method to initialize the data.

    private ObjectRef<DataObject> mDataRef;
    private DataObject mData;
    private DataObjectBool mMyBool;
    
    public DataObject getData()
    {
        return mData;
    }
    
    private void createData()
    {
        Domain domain = getDomain();
        mDataRef = DataObject.create(domain, "root");
        mData = mDataRef.get();
    
        // Add the remaining structure of the DataSource.
        try (ObjectRef<DataObjectBool> objRef = DataObjectBool.create(domain, "MyBoolean", true))
        {
            mMyBool = objRef.get();
            mData.addChild(mMyBool);
        }
    }
    
    private var mDataRef: ObjectRef<DataObject>? = null
    private var mData: DataObject? = null
    private var mMyBool: DataObjectBool? = null
    override fun getData(): DataObject {
        return mData!!
    }
    
    private fun createData() {
        mDataRef = DataObject.create(domain, "root")
        mData = mDataRef?.get()
        DataObjectBool.create(domain, "MyBoolean", true).use { objRef ->
            mMyBool = objRef.get()
            mData?.addChild(mMyBool)
        }
    }
    

To learn more about the data sources in Kanzi, see Data sources.

Creating bindings

You can use bindings to set the value of a property or property field with the value from another property, property field, or a data source.

To create a one-way binding:

// Creates a one-way binding to the Opacity property of Node A.
AbstractBinding binding =
    BindingBuilder.createOneWay("../Node A", Node2D.OpacityProperty);
// Creates a one-way binding to the Opacity property of Node A.
val binding = BindingBuilder.createOneWay("../Node A", Node2D.OpacityProperty)

To set a binding to the property of a node:

// Sets the binding to Node B. Creates a binding runtime that you can use to remove the
// binding.
AbstractBindingRuntime runtime = nodeB.setBinding(binding, Node2D.OpacityProperty);
// Sets the binding to Node B. Creates a binding runtime that you can use to remove the
// binding.
val runtime = nodeB.setBinding(binding, Node2D.OpacityProperty)

To remove a binding from a node:

// Removes from Node B the binding to the Opacity property of Node A using the binding
// runtime.
nodeB.removeBinding(runtime);
// Removes from Node B the binding to the Opacity property of Node A using the binding runtime.
nodeB.removeBinding(runtime)

To create a callback binding processor to a binding and to set a two-way binding between properties of two nodes:

// Create a two-way binding to the Opacity property of Node A.
AbstractBinding binding =
    BindingBuilder.createTwoWay("../Node A", Node2D.OpacityProperty);

// Create a callback processor that accepts only float values greater than or equal to
// 0.5.
CallbackBindingProcessor processor =
    CallbackBindingProcessor.create(getDomain(), value -> {
        if (value instanceof Float)
        {
            return ((float) value >= 0.5f);
        }
        return false;
    });

// Add a callback processor to the binding. You must do this before adding the binding
// to a node.
binding.addProcessor(processor);

// Bind from the Opacity property of Node A to the Opacity property of Node B.
AbstractBindingRuntime runtime = nodeB.setBinding(binding, Node2D.OpacityProperty);

final float opacityAa = 0.333333f;
// Setting the opacity of Node A to less than 0.5 does not change the opacity of
// Node B.
nodeA.setOpacity(opacityAa);
final float opacityBa = nodeB.getOpacity();

final float opacityAb = 0.55555f;
// Setting the opacity of Node A to a value greater than or equal to 0.5 changes the
// opacity of Node B.
nodeA.setOpacity(opacityAb);
// Create a two-way binding to the Opacity property of Node A.
val binding = BindingBuilder.createTwoWay("../Node A", Node2D.OpacityProperty)

// Create a callback processor that accepts only float values greater than or equal to 0.5.
val processor = CallbackBindingProcessor.create(domain) { value: Any? ->
    if (value is Float) {
        return@create value >= 0.5f
    }
    false
}

// Add a callback processor to the binding. You must do this before adding the binding to a
// node.
binding.addProcessor(processor)

// Bind from the Opacity property of Node A to the Opacity property of Node B.
val runtime = nodeB.setBinding(binding, Node2D.OpacityProperty)
val opacityAa = 0.333333f
// Setting the opacity of Node A to less than 0.5 does not change the opacity of
// Node B.
nodeA.opacity = opacityAa
val opacityBa = nodeB.opacity
val opacityAb = 0.55555f
// Setting the opacity of Node A to a value greater than or equal to 0.5 changes the opacity of
// Node B.
nodeA.opacity = opacityAb

To listen to property value changes using a callback processor:

// Create a one-way binding to an object's own Opacity property.
AbstractBinding binding = BindingBuilder.createOneWay(".", Node2D.OpacityProperty);

// Implement the action to be performed on change in the property as a callback
// processor.
CallbackBindingProcessor processor =
    CallbackBindingProcessor.create(getDomain(), value -> {
        Log.debug("Opacity value changed to: " + value);

        // The return does not matter here.
        return true;
    });

// Add a callback processor to the binding. You must do this before adding the binding
// to a node.
binding.addProcessor(processor);

// Add binding to the node you want to observe.
// Note the lack of target property and field.
node.setBinding(binding);
// Create a one-way binding to an object's own Opacity property.
val binding = BindingBuilder.createOneWay(".", Node2D.OpacityProperty)

// Implement the action to be performed on change in the property as a callback
// processor.
val processor = CallbackBindingProcessor.create(domain) { value: Any ->
    Log.debug("Opacity value changed to: $value")

    // The return does not matter here.
    true
}

// Add a callback processor to the binding. You must do this before adding the binding
// to a node.
binding.addProcessor(processor)

// Add binding to the node you want to observe.
// Note the lack of target property and field.
node.setBinding(binding)

To create, set, and remove bindings with an owner:

// Create two bindings.
AbstractBinding binding1 =
    BindingBuilder.createOneWay("../Node A", Node2D.OpacityProperty);
AbstractBinding binding2 =
    BindingBuilder.createOneWay("../Node A", Node2D.BackgroundBrushProperty);
// Bind to opacity and background brush properties, use Node A as owner.
AbstractBindingRuntime runtime1 =
    nodeB.setBindingWithOwner(binding1, nodeA, Node2D.OpacityProperty);
AbstractBindingRuntime runtime2 =
    nodeB.setBindingWithOwner(binding2, nodeA, Node2D.BackgroundBrushProperty);
// Remove bindings at the same time using Node A as owner.
nodeB.removeBindingsWithOwner(nodeA);
// Create two bindings.
val binding1 = BindingBuilder.createOneWay("../Node A", Node2D.OpacityProperty)
val binding2 = BindingBuilder.createOneWay("../Node A", Node2D.BackgroundBrushProperty)
// Bind to opacity and background brush properties, use Node A as owner.
val runtime1 = nodeB.setBindingWithOwner(binding1, nodeA, Node2D.OpacityProperty)
val runtime2 =
    nodeB.setBindingWithOwner(binding2, nodeA, Node2D.BackgroundBrushProperty)
// Remove bindings at the same time using Node A as owner.
nodeB.removeBindingsWithOwner(nodeA)

To create and set a modifier binding:

// Create a binding to Render Transformation property Translation X property field of
// Node A.
AbstractBinding binding = BindingBuilder.createOneWay("../Node A",
    Node2D.RenderTransformationProperty, PropertyField.PropertyFieldTranslationX);
// Bind it to Translation Y property field of the same node.
AbstractBindingRuntime runtime = nodeA.setModifierBinding(binding,
    Node2D.RenderTransformationProperty, PropertyField.PropertyFieldTranslationY);
// Create a binding to Render Transformation property Translation X property field of Node A.
val binding = BindingBuilder.createOneWay(
    "../Node A",
    Node2D.RenderTransformationProperty,
    PropertyField.PropertyFieldTranslationX
)
// Bind it to Translation Y property field of the same node.
val runtime = nodeA.setModifierBinding(
    binding,
    Node2D.RenderTransformationProperty,
    PropertyField.PropertyFieldTranslationY
)

See Bindings.

Creating Kanzi Engine Java plugins

You can separate your Java or Kotlin types into Kanzi Engine Java plugins. This allows you to use these types from Kanzi Studio and reuse them between multiple applications.

To create a Kanzi Engine plugin in Java or Kotlin:

  1. Create a class for the plugin.

    class TestPlugin extends Plugin
    {
        @Override
        public String getName()
        {
            return "testplugin";
        }
    }
    
    class TestPlugin : Plugin() {
        override fun getName(): String {
            return "testplugin"
        }
    }
    

    Tip

    The string returned from getName() must match the name of the Jar file.

  2. Register your custom types with Kanzi.

    @Override
    public void registerClasses(MetaclassRegistry metaclassRegistry)
    {
        metaclassRegistry.registerClass(MyDataSource.class);
    }
    
    override fun registerClasses(metaclassRegistry: MetaclassRegistry) {
        metaclassRegistry.registerClass(MyKotlinDataSource::class.java)
    }
    

    Tip

    You can override an existing type by using the overrideClass instead of registerClass().

  3. Load the plugin into your Android application.

    See Using Kanzi Engine Java plugins.

To learn more about creating Kanzi Engine plugins with Java or Kotlin, see Creating a Java Kanzi Engine plugin using a template and Creating a Java Kanzi Engine plugin manually.

Platform-specific code

Java plugins can run on multiple platforms, such as Kanzi Android framework (droidfw) and in the Kanzi Studio Preview. Make sure that the plugin runs platform-specific code only when it is running on a compatible platform. For example, call the Android API only when the plugin is running in an Android environment.

To detect the environment, use the Platform methods:

if (Platform.isAndroid())
{
    // Initialize the code path for when the plugin runs on Android.
    Object context = domain.getPlatformContext();
}
else
{
    // Initialize the code path for when the plugin runs in the Kanzi Studio Preview.
}
if (Platform.isAndroid()) {
    // Initialize the code path for when the plugin runs on Android.
    val context = domain.platformContext
} else {
    // Initialize the code path for when the plugin runs in the Kanzi Studio Preview.
}

Exceptions

Kanzi uses checked exceptions to indicate recoverable errors. For example, if the resource is not found when you call the Node.acquireResource method, Kanzi throws the ObjectNotFoundException exception.

Kanzi Engine exceptions thrown by native code are packaged into instances of NativeException. It is not expected for Java code to handle these exceptions. Instead, allow the exceptions to propagate up the stack so that Kanzi retranslates them to a native exceptions that Kanzi Engine can handle.

Using custom JNI extensions

You can implement custom JNI code to access areas of Kanzi Engine C++ API not yet covered by Kanzi Java API. To ease integration with JNI code, all relevant Kanzi Java API classes come with method long getNative(), which returns a pointer to their C++ counterpart. For the exact C++ type of the pointer, see the API reference for respective Kanzi Java API class.