Build an Android App Widget for Tabris.js Apps
The app widget is one of the desirable features in mobile apps and highly requested by Tabris.js developers. Unfortunately, we cannot directly use Tabris.js to create home screen widgets. However, we can create it natively as a Tabris.js custom widget.
This article covers a complete tutorial on how to create a custom home screen widget plugin for the Android platform and how to use it in your Tabris.js project. We are going to create a plugin that renders a counter, increases or decreases it directly in the widget, and exchanges the value with a Tabris.js application. Additionally, we’ll create Tabris.js project example that renders received counter value and includes the TextInput that sends the input number to the home screen widget.
Building a custom home screen widget requires some experience in Tabris.js and Android development. We recommend reading the Tabris.js documentation about creating a custom widget in both JavaScript and Android parts.
So, here we go:
We start with a Cordova plugin project structure. I suggest using the project in this article as a template for your own app widget project.
In order to reference the Tabris.js specific APIs, download the latest Tabris.js Android Cordova platform, create the environment variable TABRIS_ANDROID_PLATFORM
on your operating system, and point it to the downloaded platform root directory.
Open Android Studio and import project from the tabris-plugin-app-widget\project\android
path.
We can automatically create widget provider class and relevant resource files with the help of Android Studio.
Right click on project-> New -> Widget -> App Widget
.
Once you click on it, a frame will open. Provide a name for your widget (we named it a TabrisAppWidget).
Now Android Studio will create 3 files for you: TabrisAppWidget.kt
, tabris_app_widget.xml
, and tabris_app_widget_info.xml
. You can also create these files manually as we did: TabrisAppWidgetProvider.kt
, app_widget.xml
, and app_widget_info.xml
.
Update these classes and files respectively with the following codes:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
const val ACTION_INCREASE = "ACTION_INCREASE" const val ACTION_DECREASE = "ACTION_DECREASE" const val ACTION_OPEN_APP = "ACTION_OPEN_APP" class TabrisAppWidgetProvider : AppWidgetProvider() { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { for (appWidgetId in appWidgetIds) { updateAppWidget(context, appWidgetManager, appWidgetId) } } override fun onEnabled(context: Context) { // Enter relevant functionality for when the first widget is created } override fun onDisabled(context: Context) { // Enter relevant functionality for when the last widget is disabled } override fun onReceive(context: Context, intent: Intent?) { super.onReceive(context, intent) when (intent?.action) { ACTION_INCREASE -> AppWidgetUtil.incrementCounter(context) ACTION_DECREASE -> AppWidgetUtil.decrementCounter(context) ACTION_OPEN_APP -> AppWidgetUtil.openApp(context) } } } internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) { val resources = context.resources val packageName = context.packageName val widgetLayout = resources.getIdentifier("app_widget", "layout", packageName) val remoteViews = RemoteViews(context.packageName, widgetLayout) updateActions(context, remoteViews) appWidgetManager.updateAppWidget(appWidgetId, remoteViews) } internal fun updateActions(context: Context, remoteViews: RemoteViews) { val resources = context.resources val packageName = context.packageName val incrementId = resources.getIdentifier("increment", "id", packageName) val decrementId = resources.getIdentifier("decrement", "id", packageName) val openAppId = resources.getIdentifier("open_app", "id", packageName) updateAction(context, remoteViews, ACTION_INCREASE, incrementId) updateAction(context, remoteViews, ACTION_DECREASE, decrementId) updateAction(context, remoteViews, ACTION_OPEN_APP, openAppId) } internal fun updateAction(context: Context, remoteViews: RemoteViews, action: String, viewId: Int) { val intent = Intent(context, TabrisAppWidgetProvider::class.java).apply { this.action = action } val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) remoteViews.setOnClickPendingIntent(viewId, pendingIntent) } |
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 38 39 40 41 42 43 44 45 46 47 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#FF039BE5" android:orientation="horizontal"> <Button android:id="@+id/increment" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_weight="1" android:gravity="center" android:text="+" /> <TextView android:id="@+id/counter" android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginStart="16dp" android:layout_weight="1" android:textSize="20sp" android:textStyle="bold" android:textColor="#FFFFFF" android:gravity="center" android:text="0" /> <Button android:id="@+id/decrement" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_weight="1" android:gravity="center" android:text="-" /> <Button android:id="@+id/open_app" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" android:layout_weight="2" android:gravity="center" android:text="Open App" /> </LinearLayout> |
1 2 3 4 5 6 7 8 9 10 11 |
<?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="320dp" android:minHeight="36dp" android:updatePeriodMillis="86400000" android:previewImage="@drawable/example_appwidget_preview" android:initialLayout="@layout/app_widget" android:resizeMode="horizontal" android:widgetCategory="home_screen" android:initialKeyguardLayout="@layout/app_widget"> </appwidget-provider> |
Additionally, add the following new classes:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
class AppWidgetUtil { companion object { private const val PREFERENCE_NAME = "app_widget_name" private const val PREFERENCE_COUNTER_KEY = "counter" private const val APP_SCHEME = "com.elshadsm.appwidget" fun openApp(context: Context) { val uri = Uri.parse("$APP_SCHEME://?counter=${getCounter(context)}") Intent(Intent.ACTION_VIEW, uri).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(this) } } fun getCounter(context: Context) = context.getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE).getInt(PREFERENCE_COUNTER_KEY, 0) fun incrementCounter(context: Context) { var counter = getCounter(context) updateCounter(context, ++counter) } fun decrementCounter(context: Context) { var counter = getCounter(context) updateCounter(context, --counter) } fun updateCounter(context: Context, value: Int) { context.getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE) .edit() .putInt(PREFERENCE_COUNTER_KEY, value) .apply() updateRemoteViews(context) } private fun updateRemoteViews(context: Context) { val resources = context.resources val packageName = context.packageName val widgetLayout = resources.getIdentifier("app_widget", "layout", packageName) val counterId = resources.getIdentifier("counter", "id", packageName) val remoteViews = RemoteViews(context.packageName, widgetLayout) remoteViews.setTextViewText(counterId, getCounter(context).toString()) val appWidget = ComponentName(context, TabrisAppWidgetProvider::class.java) val appWidgetManager = AppWidgetManager.getInstance(context) appWidgetManager.updateAppWidget(appWidget, remoteViews) } } } |
1 2 3 4 5 6 7 |
class TabrisAppWidget(private val scope: ActivityScope) { var counter: Int set(value) = AppWidgetUtil.updateCounter(scope.context, value) get() = AppWidgetUtil.getCounter(scope.context) } |
Receive data from Tabris.js application
Create TabrisAppWidgetHandler.kt
class and CounterProperty.kt
object.
1 2 3 4 5 6 7 8 9 10 11 12 |
@Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") open class TabrisAppWidgetHandler(private val scope: ActivityScope) : ObjectHandler<TabrisAppWidget> { override val type = "com.elshadsm.appwidget.TabrisAppWidget" override val properties: List<Property<*, *>> by lazy { listOf(CounterProperty) } override fun create(id: String, properties: V8Object) = TabrisAppWidget(scope) } |
1 2 3 |
object CounterProperty : IntProperty<TabrisAppWidget>("counter", { it?.let { counter = it } }) |
Create TabrisAppWidget.js
file under the tabris-plugin-app-widget\www
path with the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class TabrisAppWidget extends tabris.Widget { get _nativeType() { return 'com.elshadsm.appwidget.TabrisAppWidget'; } } tabris.NativeObject.defineProperties(TabrisAppWidget.prototype, { counter: { type: 'number', nocache: true } }); module.exports = TabrisAppWidget; |
Provide details about the newly created module, sources, resources, and modifications on the AndroidManifest.xml
file in the plugin.xml
file.
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
<js-module src="www/TabrisAppWidget.js" name="appwidget"> <clobbers target="appwidget.Widget" /> </js-module> <platform name="android"> <config-file target="AndroidManifest.xml" parent="/manifest"> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> </config-file> <config-file target="AndroidManifest.xml" parent="/manifest/application"> <receiver android:name="com.elshadsm.appwidget.TabrisAppWidgetProvider" > <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/app_widget_info" /> </receiver> </config-file> <config-file target="AndroidManifest.xml" parent="/manifest/application"> <meta-data android:name="com.eclipsesource.tabris.android.HANDLER.com.elshadsm.appwidget" android:value="com.elshadsm.appwidget.TabrisAppWidgetHandler" /> </config-file> <source-file src="src/android/com/elshadsm/appwidget/TabrisAppWidgetHandler.kt" target-dir="src/com/elshadsm/appwidget" /> <source-file src="src/android/com/elshadsm/appwidget/TabrisAppWidgetProvider.kt" target-dir="src/com/elshadsm/appwidget" /> <source-file src="src/android/com/elshadsm/appwidget/TabrisAppWidget.kt" target-dir="src/com/elshadsm/appwidget" /> <source-file src="src/android/com/elshadsm/appwidget/CounterProperty.kt" target-dir="src/com/elshadsm/appwidget" /> <source-file src="src/android/com/elshadsm/appwidget/AppWidgetUtil.kt" target-dir="src/com/elshadsm/appwidget" /> <resource-file src="project/android/appwidget/src/main/res/layout/app_widget.xml" target="res/layout/app_widget.xml" /> <resource-file src="project/android/appwidget/src/main/res/xml/app_widget_info.xml" target="res/xml/app_widget_info.xml" /> <resource-file src="project/android/appwidget/src/main/res/drawable-nodpi/example_appwidget_preview.png" target="res/drawable-nodpi/example_appwidget_preview.png" /> </platform> |
Send data to Tabris.js application
We suggest using the tabris-plugin-launchmonitor in conjunction with the cordova-plugin-customurlscheme to be able to launch Tabris.js app by URL, and to read the launch parameters.
Add the following plugins to the your-tabris-js-project/cordova/config.xml
file:
1 2 3 4 5 6 |
<plugin name="cordova-plugin-customurlscheme" spec="4.3.0"> <variable name="URL_SCHEME" value="com.elshadsm.appwidget" /> </plugin> <plugin name="tabris-plugin-launchmonitor" spec="https://github.com/eclipsesource/tabris-plugin-launchmonitor.git" /> <plugin name="tabris-plugin-app-widget" spec="https://github.com/elshadsm/tabris-plugin-app-widget.git" /> |
Calling the
openApp function in the AppWidgetUtil.kt
class launches your Tabris.js app with the relevant counter
parameter.
1 2 3 4 5 6 7 8 9 |
private const val APP_SCHEME = "com.elshadsm.appwidget" fun openApp(context: Context) { val uri = Uri.parse("$APP_SCHEME://?counter=${getCounter(context)}") Intent(Intent.ACTION_VIEW, uri).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(this) } } |
Use the JavaScript codes below for your example Tabris.js project:
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 38 39 40 41 42 43 44 45 46 47 48 |
const { TextView, Composite, TextInput, Button, contentView } = require('tabris'); const widget = new appwidget.Widget(); contentView.append( new Composite({ left: 0, top: 100, right: 0 }).append( new TextView({ class: 'counter-label', left: 16, centerY: 0, text: 'Received counter value:', font: '24px' }), new TextView({ class: 'counter-value', left: ['prev()', 16], centerY: 0, right: 16, font: 'bold 24px', text: '-' }) ), new TextView({ class: 'input-label', left: 16, top: ['prev()', 64], right: 16, text: 'Update Widget' }), new Composite({ left: 0, top: ['.input-label', 4], right: 0 }).append( new TextInput({ id: 'input', left: 16, centerY: 0, right: ['50%', 8] }), new Button({ left: ['prev()', 16], centerY: 0, right: 16, text: 'Update' }).onSelect(() => handleUpdate()) ) ); function handleUpdate() { widget.counter = contentView.find('#input').first().text; } eslaunchmonitor.on({ urlLaunch: ({ url, queryParameters }) => { /* Handle the selection event of the 'Open App' button in the widget */ console.log(url); console.log(queryParameters); contentView.find('.counter-value').first().text = queryParameters.counter; } }); |
That is all. Hope this blog post helped you understand how to implement the Android app widget properly in Tabris.js projects.
And you can download the complete project from GitHub.
Feedback is welcome!
Want to join the discussion?Feel free to contribute!