Communication with WebView Using JavaScript in Android

WebView is a special component in Android which serves as kind of built-in browser inside Android applications. If you want to execute HTML, CSS or JavaScript code in your Android app, or you need to allow users visit a URL without leaving your application, WebView is the way to go. This component is not utilized very often, but, in some cases, using WebView is simply the best trade-off given the requirements.

Therefore, in this post, I’ll explain how to establish communication between the “native” code and the internals of the the WebView in Android applications, which can be surprisingly challenging task.

HTML, CSS and JavaScript for Android WebView

For the purpose of this tutorial, imagine that your WebView executes the following code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script>
        function getToastMessage() {
            return document.getElementById('toastMessage').value;
        }

        function concatenateStrings(arg1, arg2) {
            return arg1 + arg2;
        }

        function showToast() {
            MyJavascriptInterface.showToast(getToastMessage());
        }
    </script>
</head>
<body>

<div style="max-width:960px; margin: auto; text-align:center;">
    <div style="margin-top: 50px;">
        <button id="changeColor" type="button" onclick="this.style.backgroundColor='red'">Change color</button>
    </div>

    <div style="margin-top: 50px;">
        <input id="toastMessage" type="text" placeholder="Toast message"/>
    </div>

    <div style="margin-top: 50px;">
        <button id="showToast" type="button" onclick="showToast()">Show toast</button>
    </div>
</div>

</body>
</html>

Don’t worry if this functionality doesn’t make sense. I designed it for the purpose of this tutorial, so I’ll refer to specific parts of this code in the following sections.

Depending on your requirements, you can fetch the contents of the WebView from the web using webView.loadUrl("<url>") method, or you can bind the code directly (e.g. after loading it from assets) using webView.loadDataWithBaseURL("", html, "text/html", "UTF-8", null). The following approaches will work with either of these methods.

Enable WebView’s JavaScript Support

According to the above HTML, the topmost button should change its own color when you click on it. However, in practice, if you just push this code into a WebView, chances are that it’ll do nothing. The reason for this is that JavaScript support in WebView is disabled by default, so you’ll need to enable it manually. This is a security precaution motivated by the fact that executing random JavaScript code can be risky from security standpoint.

To enable JavaScript inside WebView, execute this command:

webView.settings.javaScriptEnabled = true

After that, the button should work as expected.

Evaluate External JavaScript Code inside WebView

The previous example demonstrated execution of JavaScript code contained inside the original HTML. But what if you want to “inject” the code from outside? In other words, what if you want to package the JavaScript code together with the “native” code, and then evaluate it withing the scope of the contents of the WebView at runtime? Enter webView.evaluateJavascript(script, resultCallback) method.

For example, the following native code will result in a change of WebView’s background color to blue:

webView.evaluateJavascript("document.body.style.background = 'blue';", null)

Evaluate External JavaScript Code inside WebView and Return a Value

In the previous example, I passed some JavaScript code into the WebView and evaluated it. That was one-way communication. Is there a way to use a similar approach, but also return some data back to the native side? Yes, there is!

This code will show a toast message using the contents of showToast input element inside the WebView:

webView.evaluateJavascript("(function() { return document.getElementById('toastMessage').value; })();") { returnValue ->
    Toast.makeText(requireContext(), returnValue, Toast.LENGTH_SHORT).show()
}

Note the more involved syntax of the JavaScript code here. While in the previous example I could just write the JavaScript logic itself, now I also had to enclose it into an anonymous function and evaluate that function.

In addition, if you run the above code, you’ll notice that the text shown in the toast is enclosed in double quotes. That’s because the return value from the JavaScript function is “wrapped” into JSON value, JSON object or JSON array (depending on what the function actually returns) before it’s passed to your native callback. Therefore, you’ll need to “unwrap” the return value to get the original data.

Call JavaScript Function inside WebView from Native Android Code

Now let’s discuss a bit different use case. Imagine that you know that there is a specific JavaScript function inside the WebView and you want to call it from outside. How would you go about this?

Well, the syntax is very similar, but, instead of defining your own logic, simply specify the name of the function to call:

webView.evaluateJavascript("getToastMessage();") { returnValue ->
    Toast.makeText(requireContext(), returnValue, Toast.LENGTH_SHORT).show()
}

Pass Parameters to JavaScript Function inside WebView

If you’ll be calling predefined functions inside the WebView, you might also need to pass additional parameters into them. The syntax is quite straightforward:

webView.evaluateJavascript("concatenateStrings('${arg1}', '${arg2}');") { returnValue ->
    Toast.makeText(requireContext(), returnValue, Toast.LENGTH_SHORT).show()
}

Unfortunately, the apparent simplicity of this approach is misleading. If you can guarantee that the arguments won’t contain any special characters, then you’ll be fine. Otherwise, you can run into problems. For example, if any of the arguments in the above example will contain the single quote character, the resulting JavaScript code will be invalid. In addition, if the arguments can be multi-line, you better watch out.

The solution to this problem is to escape all special characters in the arguments before you construct the JavaScript code. The details, however, are not within the scope of this article. There is a lot of info about escaping JavaScript arguments out there, so, now, that you know about this pitfall, you can search for it.

Call Native Android Code from within WebView

So far, I demonstrated how to initiate various flows involving WebView’s contents from the native side. However, in some cases, you want to do the exact opposite and call to native code from within the WebView itself. That’s possible, but the solution is a bit more involved.

First, in your native code, you need to define the “native interface” that will be exposed to JavaScript code inside the WebView:

class MyJavascriptInterface(private val context: Context) {
    @JavascriptInterface
    fun showToast(message: String) {
        Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
    }
}

The fact that this interface requires Context is just an example, but @JavascriptInterface annotation is mandatory. It basically designates the methods that JavaScript code will be able to “see”.

Once you defined this interface, you need to add it to the WebView:

webView.addJavascriptInterface(MyJavascriptInterface(requireContext()), "MyJavascriptInterface")

The first argument is the interface itself and the second argument is the name of that instance inside the WebView. It’s not required for them to be the same, but this sounds like a reasonable default choice.

After the above two steps, you’ll be able to call methods on this “native interface” from within JavaScript code inside the WebView:

function showToast() {
    MyJavascriptInterface.showToast(getToastMessage());
}

Take into account the fact that MyJavascriptInterface.showToast(message) method in your native code will be called on a special background thread, so it must be thread-safe.

Access WebView’s JavaScript Console Messages in Native Android Code

If you’ll be working with a WebView in Android, chances are that you’ll want to read its console messages. This is a nice tool for logging and basic debugging.

However, if you’ll just add this statement inside your JavaScript code, you won’t see any output:

function concatenateStrings(arg1, arg2) {
    console.log(arg1, arg2);
    ...
}

To intercept console messages inside the native code, you’ll need to use the following trick:

webView.webChromeClient = object: WebChromeClient() {
    override fun onConsoleMessage(message: String?, lineNumber: Int, sourceID: String?) {
        Log.i("WebViewConsole", message)
    }
}

In this case, I simply print console messages into Android’s logcat, but you can do whatever you want with them.

Conclusion

WebView is a niche and a bit strange component, but it does have its use cases. So, if you’re out of luck and must deal with WebViews in your application, then, at least, I hope that this article will make your journey a bit smoother.

Check Out My Courses on Udemy

Leave a Comment