Step-by-Step Guide to Creating Tabris.js Native Widgets on Android
Apache Cordova plug-ins are essential components in the app development that allow developers to use native device capabilities beyond the scope of the Tabris.js framework. Since Tabris.js uses a native UI and no HTML, most Cordova plug-ins will work with it, but not all. Plug-ins that manipulate the DOM will not work.
You can easily integrate these working plugins into your Tabris.js project. But what if you couldn’t find the one that fits your business requirements? Or what if you want to use native UI components that are not supported in Tabris.js?
In this case, you should create your own custom plugin. At first glance, it can be seen as a complicated job, but it is not difficult.
In this blog post, we are going to guide you step-by-step how to create a simple custom Cordova plugin that only works with Tabris.js. The new plugin allows users to create a Floating Action Button (FAB) on Android that represents the primary action in a Screen. We propose two ways in order to create basic scaffolding for the FAB plugin:
- Clone this template project, then customize it for your own use cases. The template project allows you to start faster but you will have to go through a lot of renaming and adjustment operations. It is easy to miss something during customization that can even cause runtime errors.
- Use the Plugman tool to create a basic project structure, then create an inner empty Android project to be able to develop relevant native sources in modern IDEs. This approach is less error-prone in customization than the previous one but requires several steps in order to create the project.
We are going to navigate through all the steps in the second approach to explain the creation of the plugin project in more detail.
Create Plugin
Install Plugman:
1 |
npm install -g plugman |
Create plugin project:
1 |
plugman create -name TabrisPluginFab -plugin_id tabris-plugin-fab -plugin_version 1.0.0 |
It generates the following structure in the current directory:
1 2 3 4 5 6 |
TabrisPluginFab │ plugin.xml │ ├───src └───www TabrisPluginFab.js |
Add Android platform:
1 |
plugman platform add –platform_name android |
Build package.json
file:
1 |
plugman createpackagejson . |
Answer the options below:
1 2 3 4 5 6 |
name: (tabris-plugin-fab) version: (1.0.0) description: Floating action button plugin for Tabris.js. git repository: https://github.com/elshadsm/tabris-plugin-fab author: Elshad Seyidmammadov license: (ISC) Revised BSD License (3-clause license) |
We will end up with the following structure:
1 2 3 4 5 6 7 8 9 10 |
TabrisPluginFab │ package.json │ plugin.xml │ ├───src │ └───android │ TabrisPluginFab.java │ └───www TabrisPluginFab.js |
JavaScript Part
Now, we are going to implement platform-independent JavaScript part of the project.
To comply with plugin naming conventions, we have renamed the project from TabrisPluginFab
to tabris-plugin-fab
.
I suggest opening the project folder in modern JavaScript IDEs like Visual Studio Code.
Clean up plugin manifest file (plugin.xml):
1 2 3 4 5 6 7 8 9 |
<?xml version='1.0' encoding='utf-8'?> <plugin id="tabris-plugin-fab" version="1.0.0" xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android"> <name>Tabris Plugin Floating Action Button</name> </plugin> |
Rename TabrisPluginFab.js
file under www
directory to FloatingActionButton.js
.
Define custom floating action button widget as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class FloatingActionButton extends tabris.Widget { get _nativeType() { return 'custom.FloatingActionButton'; } show() { return this._nativeCall('show'); } hide() { return this._nativeCall('hide'); } } tabris.NativeObject.defineProperties(FloatingActionButton.prototype, { image: { type: 'ImageValue', default: null } }); tabris.NativeObject.defineEvents(FloatingActionButton.prototype, { select: { native: true } }); module.exports = FloatingActionButton; |
The new floating action button widget supports show
and hide
methods, image
property, and select
event.
Specify the new JavaScript file in plugin.xml
:
1 2 3 |
<js-module name="FloatingActionButton" src="www/FloatingActionButton.js"> <clobbers target="custom.FloatingActionButton" /> </js-module> |
The clobbers
element is used to specify namespace under the window object, used to access the JavaScript API in our hosted Tabris.js project.
Native Android Part
We are almost done with the JavaScript API, and now it is time to create native implementation on the Android platform.
We can simply edit the TabrisPluginFab.java
file under the src/android/
directory with text editors. But it is not practical these days, especially when dealing with compilation errors.
Let us create an empty Android project in order to manage native sources in modern IDEs like Android Studio, although it is not required by the consumer or the widget. First, create the project/android
path in the root (tabris-plugin-fab
) directory.
We assume that you have downloaded and installed the Android Studio.
Open the Android Studio and select the “+ Create New Project” in the “Welcome to Android Studio” window:
Select “No Activity” in the templates window.
Configure your project in the opened “New Project” dialog as follows:
1 2 3 4 5 |
Name: android Package name: com.tabris.js.android.fab Save location: [LOCAL DIRECTORY]/tabris-plugin-fab/project/android Language: Kotlin Minimum SDK: API 21: Android 5.0 (Lollipop) |
Here is how it looks like in the “Project” window:
Now set the source directory of the new Android project to the tabris-plugin-fab/src/android
by adding the following sourceSets
block to the build.gradle
in the app
module level.
1 2 3 4 5 |
sourceSets { main { java.srcDirs = ['../../../src/android'] } } |
I suggest creating a new directory /com/tabris/js/android/fab
in the source folder and the result will be:
1 |
tabris-plugin-fab/src/android/com/tabris/js/android/fab |
Then move the TabrisPluginFab.java
file to the new directory.
The project now looks like this:
Remove TabrisPluginFab.java
and create a new FloatingActionButtonHandler.kt
file instead.
We need Tabris Android library to reference Tabris.js specific native APIs. However, the library does not exist in central repositories. So we should first download the library from the Tabris.js website and then configure the Gradle to consume it from the local repository.
Download the latest Tabris Android library from the Cordova Platforms section (sign-in required), create the environment variable TABRIS_ANDROID_PLATFORM
on your operating system, and point it to the downloaded library root directory.
Add the following maven
block to the allprojects.repositories
block in the build.gradle
at the project
level to consume Tabris Android library from the local repository.
1 2 3 |
maven { url System.getenv("TABRIS_ANDROID_PLATFORM") + "/bin/templates/project/m2repository" } |
Add Tabris Android
dependency below to the build.gradle
in the app
module level.
1 |
implementation 'com.eclipsesource.tabris.android:tabris:3.7.2' |
We have completed the Android project configuration and are now ready to update relevant native sources.
Update the FloatingActionButtonHandler.kt
file as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
package com.tabris.js.android.fab import com.eclipsesource.tabris.android.ActivityScope import com.eclipsesource.tabris.android.Property import com.eclipsesource.tabris.android.internal.nativeobject.view.ViewHandler import com.eclipsesource.v8.V8Object import com.google.android.material.floatingactionbutton.FloatingActionButton @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") class FloatingActionButtonHandler(private val scope: ActivityScope) : ViewHandler<FloatingActionButton>(scope) { override val type = "custom.FloatingActionButton" override val properties by lazy { super.properties + listOf<Property<*, *>>( ImageProperty(scope) ) } override fun create(id: String, properties: V8Object) = FloatingActionButton(scope.activity) override fun call(button: FloatingActionButton, method: String, properties: V8Object): Any? { when (method) { "show" -> button.show() "hide" -> button.hide() } return super.call(button, method, properties) } override fun listen(id: String, button: FloatingActionButton, event: String, listen: Boolean) = when (event) { "select" -> button.setOnClickListener { scope.remoteObject(it)?.notify("select") } else -> super.listen(id, button, event, listen) } } |
Note:
The value of the type
variable in the FloatingActionButtonHandler.kt
should match the value of the _nativeType
property in the FloatingActionButton.js
file.
Add a new ImageProperty.kt
file:
1 2 3 4 5 6 7 8 9 10 |
package com.tabris.js.android.fab import com.eclipsesource.tabris.android.AnyProperty import com.eclipsesource.tabris.android.Scope import com.eclipsesource.tabris.android.internal.image.Image import com.google.android.material.floatingactionbutton.FloatingActionButton class ImageProperty(private val scope: Scope) : AnyProperty<FloatingActionButton>("image", { scope.imageLoader.load(Image(scope, it)) { drawable -> setImageDrawable(drawable) } }) |
Finally, specify the following native code and modification to the AndroidManifest.xml
file for the Android platform in plugin.xml
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<platform name="android"> <config-file target="AndroidManifest.xml" parent="/manifest/application"> <meta-data android:name="com.eclipsesource.tabris.android.HANDLER.com.tabris.js.android.fab" android:value="com.tabris.js.android.fab.FloatingActionButtonHandler" /> </config-file> <source-file src="src/android/com/tabris/js/android/fab/FloatingActionButtonHandler.kt" target-dir="src/android/com/tabris/js/android/fab" /> <source-file src="src/android/com/tabris/js/android/fab/ImageProperty.kt" target-dir="src/android/com/tabris/js/android/fab" /> </platform> |
FYI:
Do not forget to include other elements as needed, such as:
<framework src="domain:dependency:version" />
when you use other Maven dependencies or bundled library projects.<resource-file src="..." target="..." />
when you use platform-specific resource files.- etc.
Type Declaration
If you want to use the plugin with TypeScript (or those that want code completion in JavaScript projects), you should define types for your API by creating the following declaration file index.d.ts
under the new types
folder.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import { EventObject, ImageValue, Listeners, Widget } from 'tabris'; declare global { namespace custom { export class FloatingActionButton extends Widget { public readonly onSelect: Listeners<EventObject<this>>; public image: ImageValue; constructor(properties: Partial<FloatingActionButton>); public show(): void; public hide(): void; } } } |
Testing Plugin
Now you can integrate the plugin into your Tabris.js project by adding the following tag to your Cordova configuration file (config.xml):
1 |
<plugin name="tabris-plugin-fab" spec="https://github.com/[YOUR REPOSITORY]/tabris-plugin-fab" /> |
Or
1 |
<plugin name="tabris-plugin-fab" spec="[LOCAL DIRECTORY]/tabris-plugin-fab" /> |
When you build your app with the snippet below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
const { Button, LayoutData, contentView } = require('tabris'); contentView.append( new custom.FloatingActionButton({ id: 'floatingActionButton', centerX: 0, centerY: 0, image: 'resources/images/plus-icon.png' }).onSelect(() => console.log('Floating Action Button Selected!')), new Button({ left: 16, right: 16, bottom: [LayoutData.next, 16], text: 'Show' }).onSelect(() => floatingActionButton.show()), new Button({ left: 16, right: 16, bottom: 16, text: 'Hide' }).onSelect(() => floatingActionButton.hide()) ); const floatingActionButton = contentView.find('#floatingActionButton').first(); |
You will get the result below:
That is all. Hopefully, this blog post encourages more people to create their own Tabris.js plugins. If you have a question or recommendation, feel free to leave a comment below.
You can download the complete project from GitHub.
Feedback is welcome!
Want to join the discussion?Feel free to contribute!
Leave a Reply
Want to join the discussion?Feel free to contribute!