Nowadays we live in the age of mobile devices. One day we can come up with an idea to build a super-mega mobile app and add it to the store. However, we can face a small problem: we are just web developers. We don’t know either Java or Objective-C and we have no time to learn it.

Today we can fulfill our dreams and improve the world we live in by building super-mega apps. This article describes the process of creating a hybrid application for Android OS.

A hybrid app combines features of web and native apps. As a native app, it can be distributed through the store and can take advantage of numerous features available on mobile devices. As a web app, it consists of HTML, CSS and JavaScript files.

This type of applications provides the following advantages:

Almost all applications can be written in JavaScript for all mobile platforms;

These apps can use some fancy features of a mobile device like a camera, an accelerometer, etc.;

All HTML, CSS and JavaScript files can be instantly updated before a new version of an app is approved.

However, a hybrid app is slower than a native app. Nevertheless, it’s not a big deal because the slowest parts of your app can be written in Java or Objective-C and added to the next update.

Now let’s build an app and change the world!

Step One. Tools for Changing The World

Let’s create our first app for Android.

First of all, we need to download Android SDK and some other stuff to emulate different Android devices or drivers to test your app on a real device. It’s quite easy: just download this package. There you will find IDE Eclipse with already added ADT plugin, Android SDK, SDK Manager, etc.

Once you get all these files, run the ‘SDK Manager.exe’ file (for Mac or Linux, you open the ‘tools/’ directory located in the Android SDK package in the terminal, then execute android sdk) and install a few things:

SDK Tools. It should have already been installed. If not, you know what to do; SDK Platform-tools; SDK for one of the Android versions. Your new Android app has to be compiled against some version of Android OS. In this article, we’ve chosen the latest version of Android so that we could use new fancy features; Drivers. If you want to test your app on your Аndroid device, you have to install Web and USB drivers (if you have some problems with debugging your apps on a real device, read this article or leave comments).

Great. The most boring part on creating apps for Android has been done. Let’s run Eclipse IDE downloaded with SDK and start changing the World. When required, you can download Android Studio (IntelliJ IDEA based IDE). Please, note that it’s still in beta and may cause an additional headache.

Step Two. Building The App

All hybrid applications are based on the WebView element. This element displays web pages with the WebKit rendering engine. It means you can create a usual HTML file with some <style> or <link> tags to include CSS and <script> tags to add JavaScript, and pass it to the WebView element. You will get almost the same picture that you can see in a browser when you open this page (see the images below).

As you probably guess, the easiest way to create a hybrid Android app is to simply open an existing web site in the WebView element. All we need is to create a WebView element, expand to full screen and pass the URL of your web site to it. Let’s do it.

First of all, you should open Eclipse that you’ve downloaded and create a new ‘Android Application Project’. Name it the way you like and choose the version of Android that you want to compile against. You may also create an icon for your new app and an activity (Note: no need to change anything in this example). When you click on the “Finish” button, your first Android app will be created.

Opened in Chrome Browser Opened in WebView

IDE automatically creates lots of files. Let’s look through some of them:

AndroidManifest.xml – this file contains information about your app and everything it can do. This file is read by Android OS when it installs and launches your app. All the information about features of the Android devices that you want to use in your app (like camera, list of contacts and so on) should be added into this file. Besides, all UI themes, controllers (activities) and background services are declared in this manifest. You can look here for more comprehensive details; res/layout{qualifier}/{name_of_your_activity}.xml – this file contains description of all UI elements that you are going to use in this activity. We need only one activity with the only one UI element (WebView) for our hybrid app. If you want to add some Android native buttons, they should be described in this file; src – this folder stores all your Java files; res/drawable{qualifier} – this folder is to contain all your graphical files; bin – this folder is to contain all your compiled stuff; assets – this folder contains all types of files you want to have access to. They might be static HTML files or some CSS and JavaScript files that will be added to the .apk of your app.

There are some drawbacks with adding all your files to the assets folder. One of them is that when you decide to change something or fix some bugs, you will have to update your app through the store because you have to compile a new .apk file. It means you’ll have to wait for approval and so on. However, if you get all files from your web server, all resources (html, css, js) may be changed without delay when they are tested properly.

Now we know everything we need for creating a simple hybrid app. First of all, we have to change the layout file (res/layout/{name_of_your_activity}.xml). We’ll add WebView element and expand to full screen.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" > <WebView android:id="@+id/WebView" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </LinearLayout> 1 2 3 4 5 6 7 8 9 10 < LinearLayout xmlns : android = "http://schemas.android.com/apk/res/android" android : layout_width = "fill_parent" android : layout_height = "fill_parent" > < WebView android : id = "@+id/WebView" android : layout_width = "fill_parent" android : layout_height = "fill_parent" / > < / LinearLayout >

We don’t need a fancy relative layout because we have only one element. That’s why we use a simple linear layout. As you can see, we declared WebView element with the ‘WebView’ id. We will use it to refer to it. The symbol “+” means that the id will be defined in the namespace of your application.

The next step is to change AndroidManifest.xml file. We need access to the Internet to download files from the web server. All we need is to add this string to the manifest file before the ‘application’ tag:

<uses-permission android:name="android.permission.INTERNET" /> 1 < uses - permission android : name = "android.permission.INTERNET" / >

The most fun part about it: write some code. Open the java file with a class for your activity (it should have the path like this “src/com.example.{your_project_name}/{your_main_activity_name}.java”) and make some changes to the code. It should look like this:

package com.example.xbsoftware; import android.os.Bundle; import android.app.Activity; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.webkit.*; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); /* make our application full screen*/ requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); /* apply our layout to the current screen */ setContentView(R.layout.activity_main); /* find WebView element by its id*/ WebView webView = (WebView) findViewById(R.id.WebView); /* create new settings for our WebView element */ WebSettings webSettings = webView.getSettings(); webSettings.setJavaScriptEnabled(true); webView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); /* here you may set URL of your site */ webView.loadUrl("https://xbsoftware.com/"); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com . example . xbsoftware ; import android . os . Bundle ; import android . app . Activity ; import android . view . View ; import android . view . Window ; import android . view . WindowManager ; import android . webkit . * ; public class MainActivity extends Activity { @ Override protected void onCreate ( Bundle savedInstanceState ) { super . onCreate ( savedInstanceState ) ; /* make our application full screen*/ requestWindowFeature ( Window . FEATURE_NO_TITLE ) ; getWindow ( ) . setFlags ( WindowManager . LayoutParams . FLAG_FULLSCREEN , WindowManager . LayoutParams . FLAG_FULLSCREEN ) ; /* apply our layout to the current screen */ setContentView ( R . layout . activity_main ) ; /* find WebView element by its id*/ WebView webView = ( WebView ) findViewById ( R . id . WebView ) ; /* create new settings for our WebView element */ WebSettings webSettings = webView . getSettings ( ) ; webSettings . setJavaScriptEnabled ( true ) ; webView . setScrollBarStyle ( View . SCROLLBARS_INSIDE_OVERLAY ) ; /* here you may set URL of your site */ webView . loadUrl ( "https://xbsoftware.com/" ) ; } }

Be careful while copying this code. The name of the package (“com.example.xbsoftware”) and the name of the class (“MainActivity”) may be different for your app. Great! All work is done and we can run our first app for Android OS. Just press “Run”. Cool, right?

Improvements for the Step Two. WebViewClient and WebChromeClient

There are two classes you should know about. First of all, it’s WebViewClient. This class is responsible for everything connected with page rendering. It has methods for catching errors, receiving HTTP requests, page loading, etc. You can overload all this methods. It means that you may add some logic for each of these events. The whole list of the supported methods for this class can be found here.

As far as you can see, if you click on some external link in your app the Chrome browser will be opened. To fix this, we will overload two methods of the WebViewClient class: ‘shouldOverrideUrlLoading’ and ‘onReceivedError’. The first one will be called when the URL is changed. The second one when you receive an error instead of some page content.

We should create a new class that extends WebViewClient and mark all the overloaded methods with the attribute ”@Override“.

We should overload the onReceivedError method, because we may want to show our own error page, in case some resources can’t be downloaded. That’s why when we get an error, we stop further downloading and open our page to handle errors from the ‘assets’ folder.

If you want to open internal pages in the WebView, you have to change the webSettings parameter. Simply add this line to your code in the place where you’ve created the webSettings object:

webSettings.setAllowFileAccess(true); 1 webSettings . setAllowFileAccess ( true ) ;

Now you can add files to the ‘assets’ folder and use them on your page. We’ve created a simple file to show errors and placed it right into the ‘assets’ folder:

You also have to add a new WebViewClient object to the existing WebView element with this code:

webView.setWebViewClient(new MyClient()); 1 webView . setWebViewClient ( new MyClient ( ) ) ;

class MyClient extends WebViewClient { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { /* will open a new web page in webview*/ view.loadUrl(url); return true; } @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { /*stop loading and show error.html page from assets folder*/ view.stopLoading(); view.loadUrl(String.format("file:///android_asset/error.html?code=%s&description=%s&url=%s", Uri.encode(String.valueOf(errorCode)), Uri.encode(description), Uri.encode(failingUrl))); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class MyClient extends WebViewClient { @ Override public boolean shouldOverrideUrlLoading ( WebView view , String url ) { /* will open a new web page in webview*/ view . loadUrl ( url ) ; return true ; } @ Override public void onReceivedError ( WebView view , int errorCode , String description , String failingUrl ) { /*stop loading and show error.html page from assets folder*/ view . stopLoading ( ) ; view . loadUrl ( String . format ( "file:///android_asset/error.html?code=%s&description=%s&url=%s" , Uri . encode ( String . valueOf ( errorCode ) ) , Uri . encode ( description ) , Uri . encode ( failingUrl ) ) ) ; } }

<!DOCTYPE html> <html> <head> <title>Achtung!</title> <link rel="stylesheet" type="text/css" href="styles.css"> </head> <body> <div id="error"> <div><span>Code:</span><span id="code"></span></div> <div><span>Description:</span><span id="description"></span></div> <div><span>Url:</span><span id="url"></span></div> </div> </body> <script type="text/javascript"> var query = window.location.search.substring(1); query.split("&").forEach(function(param){ var params = param.split("="); document.getElementById(params[0]).textContent = params[1]; }); </script> </html> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 < ! DOCTYPE html > < html > < head > < title > Achtung ! < / title > < link rel = "stylesheet" type = "text/css" href = "styles.css" > < / head > < body > < div id = "error" > < div > < span > Code : < / span > < span id = "code" > < / span > < / div > < div > < span > Description : < / span > < span id = "description" > < / span > < / div > < div > < span > Url : < / span > < span id = "url" > < / span > < / div > < / div > < / body > <script type = "text/javascript" > var query = window . location . search . substring ( 1 ) ; query . split ( "&" ) . forEach ( function ( param ) { var params = param . split ( "=" ) ; document . getElementById ( params [ 0 ] ) . textContent = params [ 1 ] ; } ) ; </script> < / html >

Let’s look at the following example:error.htmlIf you replace the URL in the loadUrl parameter with an invalid one, an error page will be opened.

The second class is WebChromeClient. It is responsible for everything connected with the browser UI. You may handle the browsing history, create new windows, manage alerts and icons. All methods that can be overloaded in this class are listed here.

For instance, let’s change every alert message dialog with our own. We have to extend the WebChromeClient class and overload the onJsAlert method. Let’s see an example:

class MyWebChromeClient extends WebChromeClient { @Override public boolean onJsAlert(WebView webView, String url, String message, JsResult result) { new AlertDialog.Builder(MainActivity.this).setTitle("hello").setMessage("hello").create().show(); return true; } } 1 2 3 4 5 6 7 class MyWebChromeClient extends WebChromeClient { @ Override public boolean onJsAlert ( WebView webView , String url , String message , JsResult result ) { new AlertDialog . Builder ( MainActivity . this ) . setTitle ( "hello" ) . setMessage ( "hello" ) . create ( ) . show ( ) ; return true ; } }

The same as with the WebViewClient, you have to add an MyWebChromeClient object to the existing WebView element:

webView.setWebChromeClient(new MyWebChromeClient()); 1 webView . setWebChromeClient ( new MyWebChromeClient ( ) ) ;

Step Two And a Half. Bind Java and JavaScript

The development process of a hybrid app won’t be so exciting, if we can’t call Java methods from JavaScript. To bind methods of some objects from Java with JavaScript, we have to use JavascriptInterface. For this example, I’ll create “MyJavaScriptInterface” class that will have one method with the name ‘make’. If you set the target SDK version to 17 or higher, all the methods accessed by JavaScript must have @JavaScriptInterface annotations.

Let’s see an example:

package com.example.xbsoftware; import android.content.Context; import android.webkit.JavascriptInterface; import android.webkit.WebView; public class MyJavaScriptInterface { private WebView webView; private Context context; public MyJavaScriptInterface(WebView currentWebView, Context currentContext){ webView = currentWebView; context = currentContext; } @JavascriptInterface public String make() throws Exception { java.lang.Thread.sleep(10000); return "Hello from Java"; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com . example . xbsoftware ; import android . content . Context ; import android . webkit . JavascriptInterface ; import android . webkit . WebView ; public class MyJavaScriptInterface { private WebView webView ; private Context context ; public MyJavaScriptInterface ( WebView currentWebView , Context currentContext ) { webView = currentWebView ; context = currentContext ; } @ JavascriptInterface public String make ( ) throws Exception { java . lang . Thread . sleep ( 10000 ) ; return "Hello from Java" ; } }

Besides, you should copy this code to add this method to the existing WebView:

MyJavaScriptInterface myInterface = new MyJavaScriptInterface(webView, MainActivity.this); /*a new property of the window object will be called MyInterface and will include function “make” */ webView.addJavascriptInterface(myInterface, "MyInterface"); 1 2 3 MyJavaScriptInterface myInterface = new MyJavaScriptInterface ( webView , MainActivity . this ) ; /*a new property of the window object will be called MyInterface and will include function “make” */ webView . addJavascriptInterface ( myInterface , "MyInterface" ) ;

Now the method ‘make’ can be accessed by JavaScript with this code:

window.MyInterface.make(); 1 window . MyInterface . make ( ) ;

It’s quite a simple example of using such a powerful tool. You can make it possible for JavaScript from WebView to communicate with all modules of the mobile devices like camera, list of contacts, WiFi and others.

If you run this example, you will see that method ‘make’ is an synchronous method. As far as you know, Javascript is single-threaded. It means that if you’re executing a Javascript block of code on a page, then no other Javascript on your page will be currently executed. In our example it means that other JavaScript code can’t be called until the “make” function is executed (it will wait ten seconds because the Java thread that executes the code connected with this function will sleep for this period of time). To solve this problem, you have to create an AsyncTask in Java and execute this code there. When everything is done, call some global function in JavaScript and pass the results. But be careful – all WebView methods can be called only in the UI thread. It means that you can’t call the loadUrl WebView method in doInBackground method because it’s in another thread.

Let’s see an example:

package com.example.xbsoftware; import android.content.Context; import android.os.AsyncTask; import android.webkit.JavascriptInterface; import android.webkit.WebView; public class MyJavaScriptInterface { private WebView webView; /*class for processing data in another thread*/ private class MyTask extends AsyncTask<Void, Void, String> { private String callback; private WebView webView; public MyTask(String callbackMethod, WebView newVebView){ callback = callbackMethod; webView = newVebView; } /* HERE you may add your computations*/ @Override protected String doInBackground(Void... smth) { String response = "Hello from Java"; try { java.lang.Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } return response; } @Override protected void onProgressUpdate(Void... progress) { } /*this method will be called when all computations in doInBackground ends */ @Override protected void onPostExecute(String response) { try { webView.loadUrl("javascript:window." + callback + "('" + response + "')"); } catch (Exception e) { e.printStackTrace(); } } } public MyJavaScriptInterface(WebView currentWebView, Context currentContext){ webView = currentWebView; } /*method that can be accessed from JavaScript*/ @JavascriptInterface public void make(final String callbackName) throws Exception { new MyTask(callbackName, webView).execute(); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com . example . xbsoftware ; import android . content . Context ; import android . os . AsyncTask ; import android . webkit . JavascriptInterface ; import android . webkit . WebView ; public class MyJavaScriptInterface { private WebView webView ; /*class for processing data in another thread*/ private class MyTask extends AsyncTask < Void , Void , String > { private String callback ; private WebView webView ; public MyTask ( String callbackMethod , WebView newVebView ) { callback = callbackMethod ; webView = newVebView ; } /* HERE you may add your computations*/ @ Override protected String doInBackground ( Void . . . smth ) { String response = "Hello from Java" ; try { java . lang . Thread . sleep ( 1000 ) ; } catch ( InterruptedException e ) { // TODO Auto-generated catch block e . printStackTrace ( ) ; } return response ; } @ Override protected void onProgressUpdate ( Void . . . progress ) { } /*this method will be called when all computations in doInBackground ends */ @ Override protected void onPostExecute ( String response ) { try { webView . loadUrl ( "javascript:window." + callback + "('" + response + "')" ) ; } catch ( Exception e ) { e . printStackTrace ( ) ; } } } public MyJavaScriptInterface ( WebView currentWebView , Context currentContext ) { webView = currentWebView ; } /*method that can be accessed from JavaScript*/ @ JavascriptInterface public void make ( final String callbackName ) throws Exception { new MyTask ( callbackName , webView ) . execute ( ) ;

The HTML file to call the method from Java:

<html> <head> <title>hello world</title> </head> <body> <div id="parent">do smth</div> <button id="ClickIt">Click me</button> </body> <script type="text/javascript"> var parent = document.getElementById("parent"); document.getElementById("ClickIt").addEventListener("click", function(){ var funcName = "globalFunction" + Math.floor(Math.random()*100); window[funcName] = function(response){ delete window[funcName]; parent.textContent = response; }; window.MyInterface.make(funcName); parent.textContent = "waiting..........."; }, false); </script> </html> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 < html > < head > < title > hello world < / title > < / head > < body > < div id = "parent" > do smth < / div > < button id = "ClickIt" > Click me < / button > < / body > <script type = "text/javascript" > var parent = document . getElementById ( "parent" ) ; document . getElementById ( "ClickIt" ) . addEventListener ( "click" , function ( ) { var funcName = "globalFunction" + Math . floor ( Math . random ( ) * 100 ) ; window [ funcName ] = function ( response ) { delete window [ funcName ] ; parent . textContent = response ; } ; window . MyInterface . make ( funcName ) ; parent . textContent = "waiting..........." ; } , false ) ; </script> < / html >

As you can see, there is no direct method to call some JavaScript functions from Java. That’s why we have to create a temporary global function to get the results. We should use the webView.loadUrl(“javascript:{your js code here}”) method to call some of the functions in javascript.

Debugging

As you’ve noticed, all Java code can be easily debugged directly in IDE. But what about JavaScript? Of course, you may debug it separately: just open it in the Chrome browser and turn on remote debugging in your desktop browser. Sometimes we need to debug all these parts together.

In that cases we should be thankful for the new Android 4.4 KitKat with its new WebView. If you have it, everything you need is to add this code:

WebView.setWebContentsDebuggingEnabled(true); 1 WebView . setWebContentsDebuggingEnabled ( true ) ;

After starting your application (of course, if you test it on a real device, it should be connected to the development machine using a USB cable), enter this URL to the address bar: chrome://inspect/#devices. Afteward you should pick your device and click on the ‘Inspect’ link.

You can find further information on the Google Android Developers site. Or, you can refer to a really interesting and useful book that inspired us to write this article.

Thanks for your attention and feel free to add your comments below.