On NativeScript for Android
On NativeScript for Android
We, at Telerik, recently announced our new solution for native cross-platform mobile development calledNativeScript. In this blog post I would like to explain some of the details for the Android platform. More specifically, I would like to explain some of the details of an important component from NativeScript, namely the JavaScript-Java bridge. For the sake of this article all use of the NativeScript is in the context of the Android platform.
A High Level Overview
Let’s start with a high-level overview and then delve into the details. The first decision that our team had to make was the choice of a JavaScript engine. Frankly, it was an easy one. We decided to use V8 because it is fast, compact and already well-proven in other projects like Node.js and Chrome. We link statically to V8 so it becomes part from *.apk package file. This prevents us from any kind of incompatibility problems and versioning issues.
The next important decision was to define the application workflow and lifecycle. We wanted to offer something familiar to the existing Android developers, so we decided to structure NativeScript around the Dalvik VM. As a matter
of fact, every NativeScript app is just a normal Android app. This means that we use the standard things like anAndroidManifest.xml
file and the
android.app.Application
class. It is just that we hide them in order to offer a better experience for cross-platform mobile development. The obvious benefits are the reuse of a familiar programming model and lifecycle events such as
onCreate
or onLowMemory
.
Let’s have a look at the AndroidManifest.xml
file.
<application
android:name="com.tns.NativeScriptApplication"
...
As you can see we use a custom application class that helps us to initialize the application and V8 engine properly. As a matter of fact there is almost no NativeScript specific code here. The important thing we do in there is to load the NativeScript native library via the standard Java API so we can use it later via JNI.
static {
System.loadLibrary("NativeScript");
}
The other important thing we do is to overwrite
onCreate
method and pass the content of a special file called bootstrap.js
(think of it as of a C-stylemain()
method) to the V8 engine. That’s it, we don’t have any other NativeScript specific logic in this class.
How Lifecycle Events are Exposed
If you are a careful reader, you probably recall that I wrote that NativeScript exposes the standard Android lifecycle app events so you may be curious how we do it. We implement this functionality by introducing the so-called “Binding classes”. These are automatically generated classes in Telerik namespaces and extend the original Android classes. Their sole purpose is so that the NativeScript framework can handle the two-way JavaScript-Java communication in a controlled manner.
For each non-final class in the Android API (and any other 3rd party class in the future) we generate this “binding.” In short, we overwrite every non-final public and protected method with the following pseudo-code implementation.
public [return_type] method_name (params) {
if (is_there_JavaScript_overwrite_for_this_method) {
return excuteJavaScriptMethod(this, “method_name”, params);
} else {
return super.method_name(params);
}
}
No magic here, it is just simple and straightforward. Let’s see the a few examples.
Suppose you want to create a new button using Java. The code you probably would write is as follows:
import android.content.Context;
import android.widget.Button;
...
Context context = …;
Button btn = new Button(context);
With NativeScript in JavaScript this code fragment would like something as:
var context = ...
var btn = new android.widget.Button(context);
Or probably you would use an alias as follows:
var Button = android.widget.Button;
var context = …;
var btn = new Button(context);
In this scenario NativeScript will create an instance ofandroid.widget.Button
and will bind it to the corresponding JavaScript variable.
Inheritance
I guess, the next question you may have is about inheritance. Let’s look at an example.
import android.widget.Button;
public class MyButton extends Button {
// overwrite method setEnabled(…) for example
}
...
MyButton btn = new MyButton(context);
We provide the following syntax in NativeScript:
var MyButton = new android.widget.Button.extends({
setEnabled: function(enabled) {
// do something
}
});
var btn = new MyButton(context);
In this scenario NativeScript will create an instance of our binding for theButton
class. It will create an instance from the following class:
package com.telerik.nativescript.android.widget;
public class Button extends android.widget.Button {
...
@Override
public void setEnabled(boolean enabled) {
if (is_there_JavaScript_overload_for_setEnabled_method) {
executeJavaScriptMethod(this, “setEnabled”, enabled);
} else {
super.setEnabled(enabled);
}
}
...
}
Interfaces
By now, you probably have a good idea what happens behind the curtains in NativeScript. We use the very same approach for Java interfaces. We generate stub classes that just forward the calls to the actual JavaScript implementation. Let’s see a concrete example.
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// Perform action on click
}
});
As before, in NativeScript we try to provide as similar a syntax as possible.
button.setOnClickListener(new android.view.View.OnClickListener({
onClick: function() {
// Perform action on click
}
}));
You just use the new
keyword on the interface name and provide the implementation object as the sole argument.
Overloaded Methods
These examples were nice and simple. So far, we haven’t talked about overloaded methods. Yeah, nasty business right. Well, I have good news for you: NativeScript supports overloaded methods as well.
The careful reader probably noticed the different method signatures for theonClick
method from previous example. The Java code uses
onClick(View v)
while the JavaScript code isonClick: function()
. I did this on purpose. I could easily define
onClick: function(v)
, but I want to emphasize that, in JavaScript functions, arguments can be accessed via thearguments
keyword. There is no such thing as overloaded methods in JavaScript. So, we map JavaScript and Java methods by
name. Hence, all overloaded Java methods would be mapped to a single JavaScript one. The developer must use thearguments
collection and do the proper method dispatch. NativeScript also maps all Java constructors to theinit
method
in JavaScript.
Type Conversion
So far, I explained some of the decisions we made in NativeScript. While it was relatively easy to map concepts like Java inheritance to JavaScript prototype-based inheritance, there are other more challenging issues. Java and JavaScript use different type systems, which makes number conversion quite challenging.
This is a subject for another blog post but for the sake of this one I will say that NativeScript provides cast-like functions (byte, short, long, etc.) that help the process of (overloaded) method resolution.
Exception Handling
The next important decision we had to make was how NativeScript would handle Java and JavaScript exceptions. We opted for automatic exception conversion. This means that you can catch Java exceptions in JavaScript and access
all members of the exception object, for example you can call the getMessage()
method from JavaScript. Accordingly, when you throw a JavaScript exception in a Java callback implementation (e.g. in an overloaded method) this JavaScript exception
would be rethrown as a special unchecked NativeScriptException
in Java. In more complex scenarios this Java exception can be propagated back in JavaScript and NativeScript will take care of translating it back to the original JavaScript object.
Here is a quick example how you can catch Java exception in JavaScript:
try {
var btn = new android.widget.Button(null);
} catch (ex) {
var msg = ex.getMessage();
// print msg
}
Multithreading
I already mentioned that Java and JavaScript differ in concepts such as inheritance and type systems. These differences can be mitigated and somehow abstracted from the developer in most cases. But there are other concepts where Java and JavaScript are really different. For example multithreading.
JavaScript does not have the concepts of threads. Java software nowadays makes extensive usage of multithreading and parallelism. So how do we map it in JavaScript?
So far, NativeScript offers limited support for this scenario. We brainstormed different ideas and we are quite optimistic that we can provide support for asynchronous and multithreading scenarios. For now, we opted to dispatch any Java-to-JavaScript call from a non-UI thread to the UI thread. This is also a very broad topic which we will cover in future blog posts.
Conclusion
NativeScript takes the task of providing the best possible experience for cross-platform mobile development using JavaScript very seriously. We try to combine the best of both the Java and JavaScript worlds. With NativeScript, developers can provide partial interface implementations and use all the dynamic features JavaScript provides. There are some trade-offs to numeric type conversions and to Java and JavaScript type systems in general. However, there is seamless support for both Java and JavaScript exceptions and there is a simple and practical initial multithreading support with many open possibilities like introducing Web Worker model and other alternatives. Stay tuned!
Mihail Slavchev works at Telerik. He likes to play with new technologies and programming languages. He used to work for outsourcing companies, but now he loves to work for product-oriented ones. In his free time he likes cycling, swimming and being with friends.