Thursday, October 2, 2014

iOS and Android - Communication between JavaScript and Native

Many of us work on hybrid applications where the core part of the functionality is written in HTML / JavaScript, loaded on a WebView. And we write native code to perform things that JS cannot do, like accessing the Gallery, writing files to the system, creating a native database, transacting with it and much more. For the application to perform seemlessly we need a bridge to call JS methods from native code and Native methods from JavaScript. PhoneGap plugins work on the same principle. In this post, I will cover ways on both Android and iOS to communicate with JavaScript.

Let us first create an HTML file, which we will load on the UI Webview and will use to interact with native
<html>
  <head>
    <script type="text/javascript">
      function loadURLInIframe(url)
      {
        document.getElementById("myFrame").src = url
      }
      function sayHello()
      {
        nativeInterface.say("Hello");
      }
</script>
</head>
  <body style="width:100%; height:100%; margin:0px;">
    <iframe id="myFrame" src="about:blank" style="width:100%; height:400px;"></iframe>
    <a onclick="sayHello()" href="#">Say Hello to Native</a>
  </body>
</html>

In the HTML code above, we have one iFrame with a blank URL. We've defined a method in JS named "loadURLInIframe", which accepts one parameter "URL" that will change the URL of the iFrame. We'd call this method from the native code. There is another span, which calls the other JS method "sayHelloToNative", which we will use to call the native methods.

Now let us write the native code to communicate with the JavaScript in the HTML above. We will start with the easier one i.e. Android. Android provides a very easy mechanism to communicate with the JS loaded on the WebView. Let us first look at a way to invoke JS method from native Java.
webView.loadUrl("javascript:loadURLInIframe(' " + "http://techiepulkit.blogspot.in" + " ');");

What are we doing here? We asked webview to loadUrl("javascript:"), which means that we are asking webView to execute a JS method. The name of the function is followed by ":" and then the parameter in brackets. Note the single quote (') on both sides of the parameter. This is important to pass a string parameter. That's it. The JS method will get called and it will load the URL in the iFrame.

Now, coming to the second part, calling native method from JavaScript. For JavaScript to be able to call native methods, we need to attach a JavaScript interface to the Webview. Let us see, how.

First, create a new class that would contain the methods to be invoked from Native.
public class JSInterfaceManager
{
    @JavascriptInterface
    public String say(String message)
        {
            Log.d("MyLogs", "Message from JS: " + message);
        }
}
In the class above, we have created one method "say" which accepts one parameter as String. Notice the @JavascriptInterface declaration on top of the method. This declaration exposes the method to JavaScript. Let us now associated this class with the interface.
webView.addJavascriptInterface(new JSInterfaceManager(), "nativeInterface");

Now that the JSInterfaceManager has been associated with the Webview, a new Object with the name "nativeInterface" will be available to the JavaScript now. Now when you click on the span "Say hello to native" in the HTML loaded in Webview, it will call nativeInterface.say method, which will invoke the say method inside the JSInterfaceManager class. Now each time you click on that span, you will get a log saying "Message from JS: Hello" in the Logcat. Note that you can also return a value in the native method and JS method will be able to receive it. That is it! We have now covered the 2 way communication with JS on Android.

Let us now repeat the same steps for iOS

First we will see calling JS method from ObjectiveC. The syntax to call a JS method from ObjectiveC looks as follows:
NSString *jsScript = [NSString stringWithFormat:@"loadURLInIframe('%@')", @"http://techiepulkit.blogspot.in"];
[webView stringByEvaluatingJavaScriptFromString: jsScript];

Pretty much similar to how it worked on Android. The only notable difference is that we don't need to prefix "javascript:" in the script to invoke the JS Method. Once this code is executed, it will load the URL in the iFrame. Simple ain't it! Well the second part i.e. calling native methods from JS is not that simple. Let's see how that works.

Before iOS 7, there was no direct way of calling native methods from JavaScript, all we had were workarounds like changing the URL of the Webview and listening the change on the delegate method. This was both slow and tedious. With iOS7, there came JavaScriptCore. JavaScript core allows the developers to create a JSContext and use it for direct communication. There are multiple uses of JavaScriptCore but I'd cover only the communication with WebView. In principle, this works quite similar to the way JavaScriptInterface on Android does but the implementation in native is quite different.
We will first define a Protocol as follows:
@protocol MyJSExport <JSExport>
  - (NSString *)say:(NSString *)message;
@end
};
We have defined a protocol which implements JSExport interface. Implementing it makes all the methods and properties in the protocol visible to JS. Now we will create a class "MyNativeInterface" that implements this protocol and has the actual implementation of the method.
@interface NativeInterface : NSObject
-- Any method not visible to JS can be defined here.
@end

@implementation NativeInterface
  - (NSString *)say:(NSString *)message
{
    NSString *printMessage = [NSString stringWithFormat:@"Message from JS: %@", message];
    NSLog(printMessage);
}
@end
Now, we have the class implementation ready and we just need to hook it with the Webview's context.
//First get the JSContext from JS.
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//
MyNativeInterface *nativeInterface = [[NativeInterface alloc] init];
context["nativeInterface"] = nativeInterface

So, we created an instance of the class that exposes the methods to JS and set it as nativeInterface on Webview's JSContext. This would mean that the JS now has a variable named "nativeInterface" and it can call it's methods. So, now when the user clicks on the span, it would invoke native method, which will log the message sent by JS. And that's it, now we have covered 2 way communication between JS and ObjectiveC on iOS as well.

Before I conclude, I would like to share a few tips, which you should keep in mind while working with JavaScriptCore:
  • JSContext can be used to invoke JS methods also. Using [context evaluateJavascript], you can call JS methods but it is erratic in nature. The application crashes randomly when using this approach so one should continue using [webView stringByEvaluatingString]
  • We generally tend to set webview's URL to "about:blank" before destroying it to force it to release memory but when used with JavaScriptCore, it works the other way round. It doesn't release the instance of Webview at all and with each instantiation, it keeps accumulating memory.
  • Do not forget to set the context references to nil on destroy. A strong reference of these objects is created and is important to be destroyed. In this case, context["nativeInterface"] = nil should be called
  • When calling native methods from JS, always wrap the call in setTimeout of 0. This ensures that the calling is done in a thread safe manner
  • When calling methods with multiple arguments, the function name gets modified when exposed to iOS. So, a method -(void) doSomething (NSString *)param withOption:(NSString *), the methodName that gets exposed will be doSomethingWithOption(arg1, arg2) instead of doSomething.

The JavaScriptCore is still in nascent stages on iOS and has a lot of quirks, still it is the fastest and the most convenient way to communicate with JS. The best part is that we are able to reuse the same approach on iOS and Android. In this post, we have just scratched the surface of JavaScriptCore and the possibilities are immense.

Do pass on any feedback or questions that you may have here.