Skip to content
This repository was archived by the owner on Dec 23, 2025. It is now read-only.

Repository files navigation

🏝 Oasis JsBridge

jitpackbuild

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 callval sum:Int= jsBridge.evaluate("new Promise(function(resolve) { resolve(1+2); })") // suspending call (JS promise)val msg:String= jsBridge.evaluate("'message'.toUpperCase()") // suspending callval 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 inferredval result2 = jsBridge.evaluateBlocking<Int>("1+2") // with explicit generic type

From Java (blocking):

Integersum = (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 parameterval i:Int= jsInt.evaluate() // suspending function + inferred generic parameterval i:Int= jsString.evaluateBlocking() // blockingval s:String= jsString.evaluate()
val o:JsonObjectWrapper= jsObject.evaluate()

From Java (blocking):

Strings = (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:

interfaceJsApi : NativeToJsInterface {
funmethod1(a:Int, b:String)
suspendfunmethod2(c:Double): String
}
val jsObject =JsValue(jsBridge, """({ method1: function(a, b) { /*...*/ }, method2: function(c) { return "Value: " + c; }})""")
// Create a native proxy to the JS objectval jsApi:JsApi= jsObject.mapToNativeObject() // no checkval jsApi:JsApi= jsObject.mapToNativeObject(check =true) // suspending, optionally check that all methods are defined in the JS objectval 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:

interfaceNativeApi : JsToNativeInterface {
funmethod(a:Int, b:String): Double
}
val obj =object:NativeApi {
overridefunmethod(a:Int, b:String): Double { ... }
}
// Create a JS proxy to the native objectval 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

KotlinJavaJSNote
Booleanboolean, Booleannumber
Intint, Integernumber
Floatfloat, Floatnumber
Doubledouble, Doublenumber
StringStringstring
BooleanArrayboolean[]Array
IntArrayint[]Array
FloatArrayfloat[]Array
DoubleArraydouble[]Array
Array<T: Any>T[]ArrayT must be a supported type
Function<R>n.a.functionlambda with supported types
Deferred<T>n.a.PromiseT must be a supported type
JsonObjectWrapperJsonObjectWrapperobjectserializes JS objects via JSON
JsValueJsValueanyreferences any JS value

Example: consuming a JS API from Kotlin

JavaScript <=> Kotlin API:

interfaceJsApi : NativeToJsInterface {
suspendfuncreateMessage(): StringsuspendfuncalcSum(a:Int, b:Int): Int
}
interfaceNativeApi : JsToNativeInterface {
fungetPlatformName(): StringfungetTemperatureCelcius(): Deferred<Float>
}

JavaScript API (js/api.js):

ES5
globalThis.createApi=function(nativeApi,config){return{createMessage: function(){constplatformName=nativeApi.getPlatformName();returnnativeApi.getTemperatureCelcius().then(function(celcius){constvalue=config.useFahrenheit ? celcius*x+c : celcius;constunit=config.useFahrenheit ? "degrees F" : "degrees C";return"Hello "+platformName+"! The temperature is "+value+" "+unit+".";});},calcSum: function(a,b){returnnewPromise(function(resolve){resolve(a+b);});}};};
ES6
globalThis.createApi=(nativeApi,config)=>{(createMessage: async()=>{constplatformName=nativeApi.getPlatformName();constcelcius=awaitnativeApi.getTemperatureCelcius();constvalue=config.useFahrenheit ? celcius*x+c : celcius;constunit=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 {
overridefungetPlatformName() ="Android"overridefungetTemperatureCelcius() = async {
// Getting current temperature from sensor or via network service37.2f
}
}

Bridging JavaScript and Kotlin:

val jsBridge =JsBridge()
jsBridge.evaluateLocalFile(context, "js/api.js")
// JS "proxy" to native APIval 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 APIval 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

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages