Using Java and Kotlin

When you want to create applications that are tightly integrated with the Android APIs, use the Java or Kotlin programming language with the Kanzi Android framework. See Developing with the Kanzi Android framework and Kanzi Java API reference.

Using properties

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

// Access a node from the view.
Node2D node = view.getRoot();

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

// Get property.
float opacity = node.getProperty(Node.OpacityProperty);
// Access a node from the view.
val node = view.getRoot()

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

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

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

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

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

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

See Property system and Reference for showing Kanzi Engine plugin custom types in Kanzi Studio.

Using messages

Kanzi API classes contain static members for built-in message types such as Button2D.ClickMessage. To dispatch these messages:

// Access a node from the view.
Node2D node = view.getRoot();

node.dispatchMessage(Button2D.ClickMessage, new MessageArguments());
// Access a node from the view.
val node = view.getRoot()

node.dispatchMessage(Button2D.ClickMessage, MessageArguments())

To dispatch a message by name, create an instance of AbstractMessageType using the full name of a message type:

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

// Access a node from the view.
Node2D node = view.getRoot();

// 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)

// Access a node from the view.
val node = view.getRoot()

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

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

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

To perform some functionality when a node receives a message, add a message handler to the node:

// Add message subscription.
button.addMessageHandler(Button2D.ToggleStateMessage,
             new Node.MessageSubscriptionFunction()
             {
                @Override
                public void handle(MessageArguments messageArguments)
                {
                    // Extract arguments.
                    int toggleState = messageArguments.getArgument(Button2D.ToggleStateProperty);
                }
             });
// Add message subscription.
button.addMessageHandler(Button2D.ToggleStateMessage,
      MessageSubscriptionFunction { messageArguments -> // Extract arguments.
          val toggleState = messageArguments.getArgument(Button2D.ToggleStateProperty)
      })

See Using messages.

Object lifetime

The ObjectRef class controls object lifetime in the Kanzi API. The native backing for the object is not reclaimed until the ObjectRef is either explicitly closed, or allowed to be garbage collected. Native objects can also have other native owners that are managed by the native portion of the Kanzi Engine. Therefore an ObjectRef does not exclusively control the lifetime of the object.

It is good practice to close all ObjectRef instances when the application no longer needs them.

Kanzi automatically wraps all newly created objects in an ObjectRef instance as the sole owner.

When you remove all references to an object, and no native references exist, the object is immediately reclaimed. If application code attempts to access the object in this state, Kanzi throws a StaleObjectException to indicate that the object is no longer valid. To prevent this, you can wrap the object in a new ObjectRef instance before that object becomes stale. The new instance extends the lifetime and prevents the native object from being reclaimed.

// 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 can throw 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 can throw a StaleObjectException.
node.setName("New Name")

To automatically manage the ObjectRef instance, use the Java try-with-resources statement or 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.
}
// Create an EmptyNode2D node.
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.
}

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 packagaged into instances of NativeException. It is not expected for Java code to handle these, instead allow them to propagate up the stack so that Kanzi retranslates it to a native exception that Kanzi Engine can handle them.

Platform-specific code

Java plugins can run on multiple platforms, such as Kanzi Android framework and in the Kanzi Studio Preview. Make sure that the application runs platform-specific code only when it is running on a compatible platform. For example, call the Android API only when the application 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.getPlatformContext()
}
else
{
    // Initialize the code path for when the plugin runs in the Kanzi Studio Preview.
}

Creating a custom node type

You can use Java or Kotlin to extend the functionality of Kanzi Engine by creating a custom node type.

To create a custom node type:

  1. Create a class in a Java project.

    class MyNode2D extends Node2D
    {
    }
    
    class MyNode2D private constructor(domain: Domain, nativeNode: Long, metaclass: Metaclass)
        : Node2D(domain, nativeNode, metaclass)
    {
    }
    
  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");
      
      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.

    • Construction. Construction of derived types must follow this pattern with a static create method and a private constructor 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);
    }
    
    private MyNode2D(Domain domain, long nativeNode, Metaclass metaclass)
    {
        super(domain, nativeNode, metaclass);
    }
    
    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.

    @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);
    
    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)
    }
    
  4. (Optional) Create custom message types.

    • Define the message type.

      @Metadata
      public static final MessageType ExampleMessage =
          new MessageType("Message.MyNode2D.Example", MessageRouting.MessageRoutingBubbling);
      
      companion object {
          ...
      
          @Metadata
          val ExampleMessage =
              MessageType("Message.MyNode2D.Example", MessageRouting.MessageRoutingBubbling)
      }
      
    • Add a message handler.

      // Add message subscription.
      addMessageHandler(MyNode2D.ExampleMessage,
                new Node.MessageSubscriptionFunction()
                {
                   @Override
                   public void handle(MessageArguments messageArguments)
                   {
                       ...
                   }
                });
      
      // Add message subscription.
      addMessageHandler(KotlinNode2D.ExampleMessage,
           MessageSubscriptionFunction { messageArguments ->
              ...
           })
      
  5. (Optional) Define custom node behavior by overriding relevant methods.

    @Override
    protected void arrangeOverride(Vector2 actualSize)
    {
        super.arrangeOverride(actualSize);
    
        ...
    }
    
    override fun arrangeOverride(actualSize: Vector2) {
        super.arrangeOverride(actualSize)
    
        ...
    }
    

Creating a custom resource manager protocol

To load resources using Java or Kotlin, implement 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 (some condition)
            {
                // Load a resource synchronously.
                Resource resource = ...;
                return new Result(resource);
            }
            else
            {
                // Use load task for asynchronous loading.
                return new Result(new ResourceManager.LoadTask() {
                    @Override
                    public void loadFunction() {
                        // Perform thread-independent loading.
                    }

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

                    @Override
                    public Resource getResult() {
                        // Return the created resource.
                    }
                });
            }
        }
    });
val resourceManager = domain.resourceManager
resourceManager.registerProtocolHandler("MyProtocol")
{ resourceManager, url, protocol, hostname, path ->
    if (some condition) {
        // Load a resource synchronously.
        val resource = ...
        ProtocolHandler.Result(resource)
    } else {
        // Use load task for asynchronous loading.
        ProtocolHandler.Result( object: ResourceManager.LoadTask {
            override fun loadFunction(resourceManager: ResourceManager) {
                // Perform thread-independent loading.
            }

            override fun finishFunction(resourceManager: ResourceManager) {
                // Create and store the resource.
            }

            override fun getResult(): Resource {
                // Return the created resource.
            }
        })
    }
}

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

By default the LoadTask will be call both the loadFunction and finishFunction. By passing ResourceLoadTaskType.FinishOnly to the constructor, the LoadTask will only call the finishFunction.

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.

    import com.rightware.kanzi.DataSource;
    
    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();
        }
    }
    
    class MyDataSource private constructor(domain: Domain, nativeHandle: Long, metaclass: Metaclass)
        : DataSource(domain, nativeHandle, metaclass) {
    
        override fun initialize() {
            super.initialize()
            createData()
        }
    
        companion object {
            @Metadata
            val metaclass = Metaclass("MyPlugin.MyDataSource")
    
            @JvmStatic
            fun create(domain: Domain, name: String): ObjectRef<MyDataSource> {
                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 val mDataRef: ObjectRef<DataObject>? = null
     private val mData: DataObject? = null
     private val mMyBool: DataObjectBool? = null
    
     override fun getData(): DataObject? {
        return mData
     }
    
     private fun createData() {
        mDataRef = DataObject.create(domain, "root")
        mData = mDataRef.get()
    
        // Add the remaining structure of the DataSource.
        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 Kanzi Engine Java plugins

You can combine custom Java or Kotlin types into a Kanzi Engine plugin.

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. Add metadata for the custom types.

    Register the metadata of all the custom Kanzi types in that plugin.

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

    Tip

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

  3. Load the plugin in your Android application.

    For example, load the plugin during the initialization of an Activity.

    @Override
    protected void onCreate(Bundle icicle)
    {
        ...
    
        KanziRuntime.Reference runtimeRef = KanziRuntime.acquire(this);
        runtimeRef.Get().getDomain().registerPlugin(new TestPlugin());
    
        ...
    }
    
    override fun onCreate(icicle: Bundle) {
        ...
    
        val runtimeRef = KanziRuntime.acquire(this)
        runtimeRef.Get().domain.registerPlugin(new TestPlugin());
    
        ...
    }
    

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