Skip to content

Evaluate JavaScript code and map values, objects and functions between Kotlin/Java and JavaScript on Android.

License

Notifications You must be signed in to change notification settings

FireStack-Lab/oasis-jsbridge-android

Β 
Β 

Repository files navigation

🏝 Oasis JsBridge

jitpack build

Evaluate JavaScript code and map values, objects and functions between Kotlin/Java and JavaScript on Android.

val jsBridge = JsBridge()
val msg: String = jsBridge.evaluate("'Hello world!'.toUpperCase()")
println(msg)  // HELLO WORLD!

Powered by:

Features

  • evaluate JavaScript code from Kotlin/Java
  • map values, objects and functions between Kotlin/Java and JavaScript
  • propagate exceptions between JavaScript and Kotlin/Java (including stack trace)
  • non-blocking API (via coroutines)
  • support for suspending functions and JavaScript promises
  • extensions (optional): console, setTimeout/setInterval, XmlHttpRequest, Promise, JS debugger

See Example.

Installation

Add jitpack repository (root gradle):

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

Add jsbridge dependency (module gradle):

implementation "com.github.p7s1digital.oasis-jsbridge-android:oasis-jsbridge-duktape:<version>"  // Duktape flavor
// OR: implementation "com.github.p7s1digital.oasis-jsbridge-android:oasis-duktape-quickjs:<version>"  // QuickJS flavor

Usage

  1. Evaluate JS code
  2. Reference any JS value
  3. Map JS objects to Kotlin/Java
  4. Map Kotlin/Java objects to JS
  5. Map JS functions to Kotlin
  6. Map Kotlin functions to JS
  7. Extensions

Evaluating JS code

Without return value:

jsBridge.evaluateNoRetVal("console.log('hello');")
jsBridge.evaluateLocalFile(context, "js/file.js")  // Android asset

With return value:

val sum: Int = jsBridge.evaluate("1+2")  // suspending call
val sum: Int = jsBridge.evaluate("new Promise(function(resolve) { resolve(1+2); })")  // suspending call (JS promise)
val msg: String = jsBridge.evaluate("'message'.toUpperCase()")  // suspending call
val obj: JsonObjectWrapper = jsBridge.evaluate("({one: 1, two: 'two'})")  // suspending call (wrapped JS object via JSON)

Blocking evaluation:

val result1: Int = jsBridge.evaluateBlocking("1+2")  // generic type inferred
val result2 = jsBridge.evaluateBlocking<Int>("1+2")  // with explicit generic type

From Java (blocking):

Integer sum = (Integer) jsBridge.evaluateBlocking("1+2", Integer.class);

Exception handling:

try {
    jsBridge.evaluate<Unit>("""
        |function buggy() { throw new Error('wrong') }
        |buggy();
    """.trimMargin())
} catch (jse: JsException) {
    // jse.message = "wrong"
    // jse.stackTrace = [JavaScript.buggy(eval:1), JavaScript.<eval>(eval:2), ....JsBridge.jniEvaluateString(Native Method), ...]
}

Note: the JS code is evaluated in a dedicated "JS" thread.

JsValue

A JsValue is a reference to any JS value.

val jsInt = JsValue(jsBridge, "123")
val jsInt = JsValue.fromNativeValue(jsBridge, 123)
val jsString = JsValue(jsBridge, "'hello'.toUpperCase()")
val jsString = JsValue.fromNativeValue(jsBridge, "HELLO")
val jsObject = JsValue(jsBridge, "({one: 1, two: 'two'})")
val jsObject = JsValue.fromNativeValue(jsBridge, JsonObjectWrapper("one" to 1, "two" to "two"))
val calcSumJs = JsValue(jsBridge, "(function(a, b) { return a + b; })")
val calcSumJs = JsValue.newFunction(jsBridge, "a", "b", "return a + b;")
val calcSumJs = JsValue.fromNativeFunction2(jsBridge) { a: Int, b: Int -> a + b }

It has an associated (global) JS variable whose name can be accessed via toString() which makes it easy to re-use it from JS code:

val sum: Int = jsBridge.evaluate("$calcSumJs(2, 3)")

The scope of a JsValue is defined by JVM. In other words, the associated global variable in JavaScript will be avalaible as long as the JsValue instance is not
garbage-collected.

Evaluating a JsValue:

val i = jsInt.evaluate<Int>()  // suspending function + explicit generic parameter
val i: Int = jsInt.evaluate()  // suspending function + inferred generic parameter
val i: Int = jsString.evaluateBlocking()  // blocking
val s: String = jsString.evaluate()
val o: JsonObjectWrapper = jsObject.evaluate()

From Java (blocking):

String s = (String) jsString.evaluateBlocking(String.class);

Additionally, a JS (proxy) value can be created from:

A JS value can be mapped to:

Using JS objects from Kotlin/Java

An interface extending NativeToJsInterface must be defined with the methods of the JS object:

interface JsApi : NativeToJsInterface {
    fun method1(a: Int, b: String)
    suspend fun method2(c: Double): String
}

val jsObject = JsValue(jsBridge, """({
  method1: function(a, b) { /*...*/ },
  method2: function(c) { return "Value: " + c; }
})""")

// Create a native proxy to the JS object
val jsApi: JsApi = jsObject.mapToNativeObject()  // no check
val jsApi: JsApi = jsObject.mapToNativeObject(check = true)  // suspending, optionally check that all methods are defined in the JS object
val jsApi: JsApi = jsObject.mapToNativeObjectBlocking(check = true)  // blocking (with optional check)

// Call JS methods from native
jsApi.method1(1, "two")
val s = jsApi.method2(3.456)  // suspending

See Example.

Note: when calling a non-suspending method with return value, the caller thread will be blocked until the result has been returned.

Using Kotlin/Java objects from JS

An interface extending JsToNativeInterface must be defined with the methods of the native object:

interface NativeApi : JsToNativeInterface {
    fun method(a: Int, b: String): Double
}

val obj = object : NativeApi {
    override fun method(a: Int, b: String): Double { ... }
}

// Create a JS proxy to the native object
val nativeApi: JsValue = JsValue.fromNativeObject(jsBridge, obj)

// Call native method from JS
jsBridge.evaluateNoRetVal("globalThis.x = $nativeApi.method(1, 'two');")

See Example.

Note: the native methods are called from the "JS" thread and must properly manage the execution context (e.g.: going to the main thread when calling UI methods). To avoid blocking the JS thread for asynchronous operations, it is possible to return a Deferred.

Calling JS functions from Kotlin

val calcSumJs: suspend (Int, Int) -> Int = JsValue
    .newFunction(jsBridge, "a", "b", "return a + b;")
    .mapToNativeFunction2()

println("Sum is $calcSumJs(1, 2)")

Available methods:

  • JsValue.mapToNativeFunctionX() (where X is the number of arguments)
  • JsValue.mapToNativeBlockingFunctionX(): blocks the current thread until the JS code has been evaluated

Calling Kotlin functions from JS

val calcSumNative = JsValue.fromNativeFunction2(jsBridge) { a: Int, b: Int -> a + b }

jsBridge.evaluateNoRetVal("console.log('Sum is', $calcSumNative(1, 2))");

Note: the native function is triggered from the "JS" thread

Extensions

Extensions can be enabled/disabled via the JsBridgeConfig given to the JsBridge constructor.

  • setTimeout/setInterval(cb, interval):
    Trigger JS callback using coroutines.delay internally.

  • console.log(), .warn(), ...:
    Append output to the logcat (or to a custom block). Parameters are displayed either via string conversion or via JSON serialization. JSON serialization provides much more detailed output (including objects and Error instances) but is slower than the string variant (which displays objects as "[object Object]").

  • XMLHtmlRequest (XHR):
    Support for XmlHttpRequest network requests using okhttp client internally. The okhttp instance can be injected in the JsBridgeConfig object. Note: not all HTTP methods are currently implemented, check the source code for details. Other network clients are not tested but should work as well (polyfill for fetch, axios uses XHR in browser mode)

  • Promise:
    Support for ES6 promises (Duktape: via polyfill, QuickJS: built-in). Pending jobs are triggered after each evaluation.

  • JS Debugger:
    JS debugger support (Duktape only via Visual Studio Code plugin)

Supported types

Kotlin Java JS Note
Boolean boolean, Boolean number
Int int, Integer number
Float float, Float number
Double double, Double number
String String string
BooleanArray boolean[] Array
IntArray int[] Array
FloatArray float[] Array
DoubleArray double[] Array
Array<T: Any> T[] Array T must be a supported type
Function<R> n.a. function lambda with supported types
Deferred<T> n.a. Promise T must be a supported type
JsonObjectWrapper JsonObjectWrapper object serializes JS objects via JSON
JsValue JsValue any references any JS value

Example: consuming a JS API from Kotlin

JavaScript <=> Kotlin API:

interface JsApi : NativeToJsInterface {
    suspend fun createMessage(): String
    suspend fun calcSum(a: Int, b: Int): Int
}

interface NativeApi : JsToNativeInterface {
    fun getPlatformName(): String
    fun getTemperatureCelcius(): Deferred<Float>
}

JavaScript API (js/api.js):

ES5
globalThis.createApi = function(nativeApi, config) {
  return {
    createMessage: function() {
      const platformName = nativeApi.getPlatformName();
      return nativeApi.getTemperatureCelcius().then(function(celcius) {
        const value = config.useFahrenheit ? celcius * x + c : celcius;
        const unit = config.useFahrenheit ? "degrees F" : "degrees C";
        return "Hello " + platformName + "! The temperature is " + value + " " + unit + ".";
      });
    },
    calcSum: function(a, b) {
      return new Promise(function(resolve) { resolve(a + b); });
    }
  };
};
ES6
globalThis.createApi = (nativeApi, config) => {(
  createMessage: async () => {
    const platformName = nativeApi.getPlatformName();
    const celcius = await nativeApi.getTemperatureCelcius();
    const value = config.useFahrenheit ? celcius * x + c : celcius;
    const unit = config.useFahrenheit ? "degrees F" : "degrees C";
    return `Hello ${platformName}! The temperature is ${value} ${unit}.`;
  },
  calcSum: async (a, b) => a + b
});

Kotlin API:

val nativeApi = object: NativeApi {
    override fun getPlatformName() = "Android"
    override fun getTemperatureCelcius() = async {
        // Getting current temperature from sensor or via network service
        37.2f
    }
}

Bridging JavaScript and Kotlin:

val jsBridge = JsBridge()
jsBridge.evaluateLocalFile(context, "js/api.js")

// JS "proxy" to native API
val nativeApiJsValue = JsValue.fromNativeObject(jsBridge, nativeApi)

// JS function createApi(nativeApi, config)
val config = JsonObjectWrapper("debug" to true, "useFahrenheit" to false)  // {debug: true, useFahrenheit: false}
val createJsApi: suspend (JsValue, JsonObjectWrapper) -> JsValue
    = JsValue(jsBridge, "createApi").mapToNativeFunction2()
    
// Create native "proxy" to JS API
val jsApi: JsApi = createJsApi(nativeApiJsValue, config).mapToNativeObject()

Consume API:

val msg = jsApi.createMessage()  // (suspending) "Hello Android, the temperature is 37.2 degrees C."
val sum = jsApi.calcSum(3, 2)  // (suspending) 5

License

Copyright (C) 2018-2020 ProSiebenSat1.Digital GmbH
🏝 Oasis Player team

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Originally based on Duktape Android (Apache license, version 2.0)
Copyright (C) 2015 Square, Inc.

Includes C code from Duktape (MIT license)
Copyright (c) 2013-2020 by Duktape authors

Includes C code from QuickJS (MIT license)
Copyright (c) 2017-2020 Fabrice Bellard
Copyright (c) 2017-2010 Charlie Gordon

About

Evaluate JavaScript code and map values, objects and functions between Kotlin/Java and JavaScript on Android.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • C++ 60.3%
  • C 34.9%
  • Kotlin 4.5%
  • Shell 0.2%
  • Java 0.1%
  • CMake 0.0%