Mikael
Tennhammar

Native web app on Android, Part 1

Senior System Developer

How smart can ’IT’ be?


There are a lot of tools and framework that enables building mobile applications for multiple platforms writing the code just once. Usually that is javascript code, or at least that is what is generated for the UI and application logic parts. The Javascript then runs in a Web container.

The old Java slogan “Write once and run everywhere” is actualized again.

Those tools works just fine, you get a mobile application for all major platforms fast, especially if the application shall serve the user with content from a back end server, things you could do from a web application.

There are things when you would like to more specific things that might not be supported across all platforms, and you will loose the control over how things is done, under the hood,

but these tools let you do a lot, and how is that even possible? accessing the camera, sending SMS, connecting Bluetooth devices,  and even interacting with telephone calls?

In addition to the javascript part there is a native part of the application that allows this integration with device.

I will show how you can interact with the Android application from you Web application running in a WebView. I’ll using AndroidStudio but I will use very little of its auto generating support, so it should be able to follow with any setup.

 

The Android application

Start with creating an empty project in AndroidStudio, with no Activity. I start to create the application by creating the main layout, res/layouts/main.xml with the following content.

<FrameLayout 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"

  android:background="#0099cc">   

  <WebView

      android:id="@+id/main_screen"

      android:layout_width="match_parent"

      android:layout_height="match_parent">

  </WebView>

</FrameLayout>

That is just a WebView filling all its parents space. Next I create the MainActivity.java

public class MainActivity extends AppCompatActivity {

  private static final String TAG = MainActivity.class.getSimpleName();

  private WebView webView;  

  @Override

  protected void onCreate(Bundle savedInstanceState) {

      super.onCreate(savedInstanceState);

      setContentView(R.layout.main);

      webView = (WebView) findViewById(R.id.main_screen);

      webView.getSettings().setJavaScriptEnabled(true);

    }

} 

To be able to for the app to be run in Android you need to tell that the MainActivity shall be the activity to launch when the application is created and started on an android device, this is done by adding the activity in the AndroidManifest.xml file inside the <application> like this.

<activity android:name=".MainActivity">

  <intent-filter>

      <action android:name="android.intent.action.MAIN" />

      <category android:name="android.intent.category.LAUNCHER" />

  </intent-filter>

</activity>

To get real full screen, not necessary but gives real fullscreen,  I’ll hide the ActionBar by adding in the onCreate method

ActionBar actionBar = getSupportActionBar();

if (actionBar != null) {

actionBar.hide();

}

and in res/values.styles.xml add

<style name="FullScreen" parent="Theme.AppCompat.Light.DarkActionBar">

  <item name="android:windowNoTitle">true</item>

  <item name="android:windowActionBar">false</item>

  <item name="android:windowFullscreen">true</item>

  <item name="android:windowContentOverlay">@null</item>

</style> 

and in the AndroidMainfest.xml in the <application> tag add following attribute

android:theme="@style/FullScreen"

 

The Javascript Interface

For the communication from the web application to the Android application I create a small class with one annotated function which will be the one that will be callable from the JavaScript code

public class JSWrapper {

  private static final String TAG = MainActivity.class.getSimpleName();

  private Context context;

  public JSWrapper(Context context) {

      this.context = context;

  }

  @JavascriptInterface

  public void onJavaScriptAction(String action){

      Toast.makeText(context, "JS says: " + action, Toast.LENGTH_LONG).show();

  }

}

To do the actual connection or binding I add these two rows in the onCreate method of the MainActivity.java

webView.getSettings().setJavaScriptEnabled(true);

JSWrapper jsWrapper = new JSWrapper(this);

webView.addJavascriptInterface(jsWrapper, "jsWrapper");

 

The Web application

So now I create my highly advanced web application, it consists of two html-files.

<html>

 <head>

 </head>

 <body>

   <p style="padding: 150px;" onClick="jsWrapper.onJavaScriptAction('goForward')">

     <button>Click Me to advance</button>

   </p>

 </body>

</html>

<html>

 <head>

 </head>

 <body>

   <p style="padding: 150px;" onClick="jsWrapper.onJavaScriptAction('goBack')">

     <button>Go back</button>

   </p>

 </body>

</html>

and I call them forward.html and back.html. To include them in the application I create an asset directory with main target (Right click on the app folder choose new->Folder (with android icon) asset folder) where I save the two files




 

I create two constants, in MainActivity.java, representing the two html files

private static final String FORWARD_HTML_FILE = "file:///android_asset/forward.html";

private static final String BACK_HTML_FILE = "file:///android_asset/back.html";

I create an Interface call Navigator.java, that representing the navigation capabilities of the application.

public interface Navigator {

  void onBack();

  void onForward();

}

I then change the JSWrapper.java to take a Navigator as a parameter in the constructor and call the Navigator when actions are received from the Javascript.

public class JSWrapper {

  private Context context;

  private Navigator navigator;

  public JSWrapper(Context context, Navigator navigator) {

      this.context = context;

      this.navigator = navigator;

  }

  @JavascriptInterface

  public void onJavaScriptAction(String action){

      Toast.makeText(context, "JS says: " + action, Toast.LENGTH_LONG).show();

      if (action.equals("goForward")){

          navigator.onForward();

      } else if (action.equals("goBack")){

          navigator.onBack();

      }

  }

}

In MainActivity.java I construct the JSWraper with an anonymous Navigator implementation, it will call a doLoadPage(String path) simply because you can not load new content from another thread then the UI thread, and the Javascript callback is running in a separate thread.

JSWrapper jsWrapper = new JSWrapper(this, new Navigator() {

  public void onBack() {

      doLoadPage(FORWARD_HTML_FILE);

  }

  public void onForward() {

      doLoadPage(BACK_HTML_FILE);

  }

});

private void doLoadPage(final String path) {

  runOnUiThread(new Runnable() {

      @Override

      public void run() {

          webView.loadUrl(path);

      }

  });

}

And finally give the WebView an initial page last in the onCreate of MainActivity.java.

webView.loadUrl(FORWARD_HTML_FILE);

Now the navigation logic is controlled by the Android application and the Web application is just the UI. Of course in the real world Javascript applications is fully capable to handle much more of the application logic and for the approach to support all major platforms with one code base you would probably using a React-Flux/Redux application set up for the Javascript parts and only minimize the Android application. Next I will show the communication the other way around, from Android to Javascript.