Leveraging the power of Web Apps in Native Android

Ankita Bajaj
Sequoia.com
6 min readJul 13, 2020

--

When it comes to mobile app development on Android or iOS, there are 3 different approaches: Native app development, Mobile web-app development better known as Progressive Web Apps(PWAs) and Hybrid app development. Among the three options, Native application development is still on top since it benefits from full access to the device’s hardware and its features, like camera, GPS, bluetooth, etc., whereas Progressive Web Apps are web apps that bring a native app-like user experience to cross-platform web applications. PWAs are becoming quite common these days. They are relatively ease to deploy, maintain and backward compatibility is not required. But there are some drawbacks. For eg: PWAs do not have full hardware access on your device. The choice between Progressive Web Apps and Native Android ultimately depends on the app requirements.

However, there may come a time when you want to utilise an existing web-app to avoid the effort of rewriting the views in native. The web-app integration in native is done via Android’s Webview class. The requirement may vary from simply rendering the web-app inside the native to doing a two-way communication between them. For two-way communication Webview’s support for Javascript and JavaScriptInterface comes into picture and that is the main focus of this blog.

This blog talks more about Native Android application development and how we can plug in web based components inside the native for faster development iteration. It is aimed at people who are developing apps in Android ecosystem

Following code describes how to achieve the two-way communication. Let’s break it down to simple scenarios and look into each of them with an example.

Following code describes how to achieve the two-way communication. Let’s break it down to simple scenarios and look into each of them with an example.

Scenario 1: Load a new url in Webview on click of a html button using a direct call to JavascriptInterface.

index.html

Let’s create a sample webpage first. You can add this to the assets folder under main.

<html>
<body>
<input type="button" onclick="NativeHandler.callApi()" style="height:100px; white-space: normal;text-align:left;" value="[JavaScript -> Java] Call api and load url" /><br/><p id="test" style="overflow-wrap:break-word">Demo App</p></body>
</html>

xml layout

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.sequoia.android.activity.
WebviewActivity">
<WebView
android:id="@+id/common_web_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>

JavascriptInterface

class DemoJavaScriptInterface(val activity: BaseActivity) {companion object {
private val TAG = DemoJavaScriptInterface::class.java.simpleName
}
@JavascriptInterface
fun callApi() {
Log.v(TAG, "call to native")
activity.runOnUiThread({ activity.callApi() })
}
}

WebviewActivity

class WebviewActivity : BaseActivity() {companion object {
private val TAG = WebviewActivity::class.java.simpleName
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.webview_activity)
common_web_view.addJavascriptInterface(DemoJavaScriptInterface(this), "NativeHandler")
initWebViewSettingsAndClient("file:///android_asset/index.html")
}
@SuppressLint("SetJavaScriptEnabled")
private fun initWebViewSettingsAndClient(url: String) {
if (isDestroyed || isFinishing) return
val
settings: WebSettings = common_web_view.getSettings()
settings.javaScriptEnabled = true
settings.javaScriptCanOpenWindowsAutomatically = true
common_web_view.setWebChromeClient(object : WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
Log.d(TAG, consoleMessage.message() + " -- From line "
+ consoleMessage.lineNumber() + " of "
+ consoleMessage.sourceId())
return super.onConsoleMessage(consoleMessage)
}
})
common_web_view.loadUrl(url)
}
override fun callApi() {
val url = "https://github.com"
loadHtmlInWebView(url)
}
@SuppressLint("SetJavaScriptEnabled")
private fun loadHtmlInWebView(url: String) {
common_web_view.loadUrl(url)
}
}

Here on click of button in html creates a call to callAPi function in JavascriptInterface which we had set with addJavascriptInterface to the Webview in activity. We also need to set javaScriptEnabled to true explicitly on the Webview.

Scenario 2: Load a new url in Webview on click of a html button using an indirect call to JavascriptInterface via script function in html.

index.html

<html>
<head>
<script type="text/javascript">
function testSync() {
NativeHandler.callApi();
}
</script>
</head>
<body>

<input type="button" onclick="testSync()" style="height:100px; white-space: normal;text-align:left;" value="[Sync JavaScript -> Java] Call api and load url" /><br/>
<p id="test" style="overflow-wrap:break-word">Demo App</p></body>
</html>

Here the only thing that changes is the on click handling in html. We now call a script function to call the callApi method instead of calling it directly.

Scenario 3: Modifying data in Webview- Change text on click of a html button

index.html

To the above html just add the following script function

function setUrlInText(url) {
if(typeof(url) == 'string') {
document.getElementById("test").innerHTML = url;
console.log("setUrlInText function entered");
}
}

This function will be called by the Webview to set new text in html.

WebviewActivity

Modify the loadHtmlInWebView method to call the new script function setUrlInText and pass new url as parameter.

@SuppressLint("SetJavaScriptEnabled")
private fun loadHtmlInWebView(url: String) {
common_web_view.evaluateJavascript("setUrlInText('"+ url+"')", null)
}

This will set the new text in the particular elementId. Here we are passing null in the callback. Scenario5 demonstrates how to handle javascript callbacks.

Scenario 4: Set the data in native and pass the entity to Webview and display its properties in html.

UserDetails

Let’s create a simple data class of UserDetails with name, email and phoneNumber property.

data class UserDetails(val name: String, val email: String, val phoneNumber: String)

index.html

To the above html just add the following script function

function getData(userDetails) {
if(typeof(userDetails) == 'object') {
var name = userDetails.name || '';
var phoneNumber = userDetails.phoneNumber || '';
var email = userDetails.email || '';
document.getElementById("test").innerHTML = "name = " + name + ", " +
"email = " + email + ", " +
"phoneNumber = " + phoneNumber;
console.log("getData function exiting " + name + ", " + email + " , " + phoneNumber);
}
}

This will set the UserDetails in the particular elementId. Here we are passing null in the callback.

xml layout

In xml layout add 3 fields to input data and a button to send data to Webview.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.sequoia.android.activity.
WebviewActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:id="@+id/etName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="Enter Name"
android:inputType="textPersonName" />
<EditText
android:id="@+id/etEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="Enter Email"
android:inputType="textEmailAddress" />
<EditText
android:id="@+id/etPhone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="Enter Phone Number"
android:inputType="phone" />
<Button
android:id="@+id/sendData"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="sendDataToWebview"
android:text="Send data to webview" />
</LinearLayout>
<WebView
android:id="@+id/common_web_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</RelativeLayout>

WebviewActivity

In the activity add the following function to handle on click of native button.

fun sendDataToWebview(view: View) {
val name = etName.text.toString()
val email = etEmail.text.toString()
val phone = etPhone.text.toString()
val userDetails = UserDetails(name, email, phone)
val strUserDetails: String = Gson().toJson(userDetails)
common_web_view.evaluateJavascript(
"getData($strUserDetails)", null
)
}

evaluateJavascript is used to call the html script function getData and passes the stringified object to its params.

Scenario 5: Modify data received in html and send it back to native and display it.

index.html

Add the following code to the getData script function. We’ll now return a modified UserDetails object.

userDetails.name = 'Demo App';
userDetails.email = 'demo@google.com';
userDetails.phoneNumber = 789;
return userDetails;

WebviewActivity

Add the following code to sendDataToWebview function.

common_web_view.evaluateJavascript(
"getData($strUserDetails)", object : ValueCallback<String> {
override fun onReceiveValue(value: String?) {
val userDetails = Gson().fromJson(value, UserDetails::class.java)
etName.setText(userDetails.name)
etEmail.setText(userDetails.email)
etPhone.setText(userDetails.phoneNumber)
Log.e(TAG, value)
}
})

We now pass a callback value to evaluateJavascript method and data from script function is received in onReceiveValue. We use this object to set new data in native view.

In this blog we have looked at a few examples which cover most of the implementation details of Native and Web-App communication. The use cases covered here were limited but you can extend these further to cover any complex requirement that you may have.

You can view the complete code for this article here.

Feel free to reach out to me incase you have any queries or have feedback on this post — ankita.bajaj@sequoia.com

Here is my profile on LinkedIn.

--

--

Ankita Bajaj
Sequoia.com

Sharing my learnings here that could benefit mobile developers