Evaluate JavaScript code and map values, objects and functions between Kotlin/Java and JavaScript on Android.
val jsBridge = JsBridge(JsBridgeConfig.bareConfig(), context)
val msg: String = jsBridge.evaluate("'Hello world!'.toUpperCase()")
println(msg) // HELLO WORLD!
Powered by:
- 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, JVM
See Example.
Add jitpack repository (root gradle):
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
Add jsbridge dependency (module gradle):
implementation "de.prosiebensat1digital.oasis-jsbridge-android:oasis-jsbridge-quickjs:<version>" // QuickJS flavor
// OR: implementation "de.prosiebensat1digital.oasis-jsbridge-android:oasis-jsbridge-duktape:<version>" // Duktape flavor
- Evaluate JS code
- Reference any JS value
- Using JS objects from Kotlin/Java
- Using Kotlin/Java objects from JS
- Calling JS functions from Kotlin
- Calling Kotlin/Java functions from JS
- Wrap a Java object in JS code
- ES6 modules
- Extensions
Suspending calls:
// Without return value:
jsBridge.evaluate<Unit>("console.log('hello');")
jsBridge.evaluateFileContent("console.log('hello')", "js/test.js")
// 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
Fire-and-forget evaluation:
jsBridge.evaluateUnsync("console.log('hello');")
jsBridge.evaluateFileContentUnsync("console.log('hello')", "js/test.js")
From Java (fire-and-forget and blocking):
jsBridge.evaluateUnsync("console.log('hello');");
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(Java Method), ...]
}
Note: the JS code is evaluated in a dedicated "JS" thread.
A JsValue is a reference to any JS value.
val jsInt = JsValue(jsBridge, "123")
val jsInt = JsValue.fromJavaValue(jsBridge, 123)
val jsString = JsValue(jsBridge, "'hello'.toUpperCase()")
val jsString = JsValue.fromJavaValue(jsBridge, "HELLO")
val jsObject = JsValue(jsBridge, "({one: 1, two: 'two'})")
val jsObject = JsValue.fromJavaValue(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.createJsToJavaProxyFunction2(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-to-Java proxy object via
JsValue.createJsToJavaProxy()
. - a JS-to-Java proxy function via
JsValue.createJsToJavaProxyFunction()
.
And JS objects/functions can be accessed from Java/Kotlin using:
- a Java-to-JS proxy object via
JsValue.createJavaToJsProxy()
. - a Java-to-JS proxy function via
JsValue.createJavaToJsProxyFunctionX()
.
An interface extending JavaToJsInterface
must be defined with the methods of the
JS object:
interface JsApi : JavaToJsInterface {
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 Java proxy to the JS object
val jsApi: JsApi = jsObject.createJavaToJsProxy() // no check
val jsApi: JsApi = jsObject.createJavaToJsProxy(check = true) // suspending, optionally check that all methods are defined in the JS object
val jsApi: JsApi = jsObject.createJavaToJsProxy(check = true) // blocking (with optional check)
// Call JS methods from Java
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.
An interface extending JsToJavaInterface
must be defined with the methods of the
Java object:
interface JavaApi : JsToJavaInterface {
fun method(a: Int, b: String): Double
}
val obj = object : JavaApi {
override fun method(a: Int, b: String): Double { ... }
}
// Create a JS proxy to the Java object
val javaApi = JsValue.createJsToJavaProxy(jsBridge, obj)
// Call Java method from JS
jsBridge.evaluate("globalThis.x = $javaApi.method(1, 'two');")
See Example.
Note: the Java 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.
val calcSumJs: suspend (Int, Int) -> Int = JsValue
.newFunction(jsBridge, "a", "b", "return a + b;")
.createJavaToJsProxyFunction2()
println("Sum is $calcSumJs(1, 2)")
Available methods:
JsValue.createJavaToJsProxyFunctionX()
(where X is the number of arguments)JsValue.createJavaToJsBlockingProxyFunctionX()
: blocks the current thread until the JS code has been evaluated
val calcSumJava = JsValue.createJsToJavaProxyFunction2(jsBridge) { a: Int, b: Int -> a + b }
jsBridge.evaluate("console.log('Sum is', $calcSumJava(1, 2))");
Note: the Java function is triggered from the "JS" thread
It is possible to wrap a Java object in JS code. The Java object itself cannot be directly used from JS but can be passed again to Kotlin/Java when needed.
val javaObject = android.os.Binder("dummy")
val jsJavaObject = JsValue.fromJavaValue(subject, JavaObjectWrapper(javaObject))
val javaObjectBack: JavaObjectWrapper<android.os.Binder> = jsJavaObject.evaluate()
assertSame(javaObjectBack, javaObject)
ES6 modules are fully supported on QuickJS. This is done by:
- Evaluating files as modules:
Example:
jsBridge.evaluateLocalFile(context, "js/module.js", false, JsBridge.JsFileEvaluationType.Module)
- Defining a custom module loader which is triggered as needed to get the JS code of a given module:
Example:
jsBridge.setJsModuleLoader { moduleName -> "<module_content>" }
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 usingokhttp
client internally. Theokhttp
instance can be injected in theJsBridgeConfig
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. -
LocalStorage:
Built-in support for browser-like local storage. UseJsBridgeConfig.standardConfig(namespace)
to initialise the local storage extension using a namespace for separation of saved data between multiple JsBridge instances. Note: If you use your own implementation of local storage you should disable this extension! -
JS Debugger:
JS debugger support (Duktape only via Visual Studio Code plugin) -
JVM config:
Offers the possibility to set a custom class loader which will be used by the JsBridge to find classes.
Kotlin | Java | JS | Note |
---|---|---|---|
Boolean |
boolean , Boolean |
number |
|
Byte |
byte , Byte |
number |
|
Short |
short , Short |
number |
|
Int |
int , Integer |
number |
|
Long |
long , Long |
number |
|
Float |
float , Float |
number |
|
Double |
double , Double |
number |
|
String |
String |
string |
|
BooleanArray |
boolean[] |
Array |
|
ByteArray |
byte[] |
Array |
|
IntArray |
int[] |
Array |
|
FloatArray |
float[] |
Array |
|
DoubleArray |
double[] |
Array |
|
Array<T: Any> |
T[] |
Array |
T must be a supported type |
List<T: Any> |
`List | Array |
T must be a supported type. Backed up by ArrayList. |
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 |
JavaObjectWrapper |
JavaObjectWrapper |
object |
serializes JS objects via JSON |
JsValue |
JsValue |
`any | references any JS value |
JsToJavaProxy<T> |
JsToJavaProxy |
object |
references a JS object proxy to a Java interface |
Any? |
Object |
dynamically mapped to a string, number, boolean, array or wrapped Java objects |
JavaScript <=> Kotlin API:
interface JsApi : JavaToJsInterface {
suspend fun createMessage(): String
suspend fun calcSum(a: Int, b: Int): Int
}
interface JavaApi : JsToJavaInterface {
fun getPlatformName(): String
fun getTemperatureCelcius(): Deferred<Float>
}
JavaScript API (js/api.js):
ES5
globalThis.createApi = function(javaApi, config) {
return {
createMessage: function() {
const platformName = javaApi.getPlatformName();
return javaApi.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 = (javaApi, config) => {(
createMessage: async () => {
const platformName = javaApi.getPlatformName();
const celcius = await javaApi.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 javaApi = object: JavaApi {
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(JsBridgeConfig.standardConfig("namespace"), context)
jsBridge.evaluateLocalFile(context, "js/api.js")
// JS "proxy" to Java API
val javaApiJsValue = JsValue.createJsToJavaProxy(jsBridge, javaApi)
// JS function createApi(javaApi, config)
val config = JsonObjectWrapper("debug" to true, "useFahrenheit" to false) // {debug: true, useFahrenheit: false}
val createJsApi: suspend (JsValue, JsonObjectWrapper) -> JsValue
= JsValue(jsBridge, "createApi").createJavaToJsProxyFunction2()
// Create Java "proxy" to JS API
val jsApi: JsApi = createJsApi(javaApiJsValue, config).createJavaToJsProxy()
Consume API:
val msg = jsApi.createMessage() // (suspending) "Hello Android, the temperature is 37.2 degrees C."
val sum = jsApi.calcSum(3, 2) // (suspending) 5
Until we have a proper Changelog file, you can use a convenient way to see changes between 2 tags in GitHub, e.g.
https://github.com/p7s1digital/oasis-jsbridge-android/compare/0.13.0...0.14.2
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