Skip to content

VO2Group/Starter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

90 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Starter

Starter is a starter kit for building hybrid apps, it contains:

  • An HTML5 application
  • Two native projects
  • And a set of tools files

Platforms

Starter focuses to two platforms:

  • iOS 9+
  • Android 6.0+ (API level 23)

If you need more backward compatibility or more exotic platforms like Windows Phone or BlackBerry, use something else!

Tools

Following tools are mandatory for a full use of Starter:

  • XCode: Even if you don't feel right with it, there is no other choice for iOS.
  • Android Studio: The best IDE for building Android apps.
  • GNU make: After more than 25 years, the old make build tool still rule them all!
  • Jenkins: The king of continuous integration.
  • fastlane: The game changer for stores submission.

Concepts

Native projects are not generated!

If you use Starter you have to modify manually the native projects, they are located in platforms directory and they are both named AppShell.

Native projects use WebKit

Both projects are Single View Applications with a Fullscreen WebView:

More precisely Starter uses the method loadFileURL of WKWebView class introduced in iOS 9!

Native projects dispatch events to DOM Document Object

Android and iOS are multitasking platforms, applications can be paused and can be resumed. To handle these features Starter sends some events from native code to Javascript. The events are named pause and resume.

On Android events are dispatched by the com.starter.appshell.MainActivity like this:

this.mWebView.evaluateJavascript("document.dispatchEvent(new Event('pause'));", null);
this.mWebView.evaluateJavascript("document.dispatchEvent(new Event('resume'));", null);

And on iOS by the ViewController like this:

self.webView!.evaluateJavaScript("document.dispatchEvent(new Event('pause'));", completionHandler: nil)
self.webView!.evaluateJavaScript("document.dispatchEvent(new Event('resume'));", completionHandler: nil)

Finally events are handled in Javascript like this:

document.addEventListener('pause', function (e) {...});
document.addEventListener('resume', function (e) {...});

Native projects expose native to Javascript bridge

In hybrid applications, Javascript needs to call some native code. To do this, the native projects inject an object called platform in Window object before loading HTML.

On Android platform object look like this:

window.platform = {
  name: function () {
    return 'android';
  },

  foo: function (message) {
    android.foo(message);
  },

  bar: function (message, callback) {
    var uuid = this._uuid();
    this._callbacks[uuid] = callback;
    android.bar(message, uuid);
  },

  _callbacks: {},

  _uuid: function () {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
      var r = Math.random() * 16 | 0;
      var v = c == 'x' ? r : (r & 0x3 | 0x8);
      return v.toString(16);
    });
  },

  _invoke: function (uuid, err, data) {
    this._callbacks[uuid](err, data);
    delete this._callbacks[uuid];
  },
};

The android object is introduced by the addJavascriptInterface method of android.webkit.WebView class. Also android.foo() and android.bar(...) functions are defined by the methods of com.starter.appshell.JavascriptInterface class (see android.webkit.JavascriptInterface annotation). Last but not the least, _callbacks, _uuid and _invoke are private properties, they are used to support async function callback.

And com.starter.appshell.MainActivity injects it like this:

try (InputStream stream = this.getAssets().open("platform.js")) {
    byte[] buffer = new byte[stream.available()];
    stream.read(buffer);
    this.mWebView.evaluateJavascript(new String(buffer), null);
}
catch (IOException ex) {
}

On iOS, things are quite the same, platform object looks like this:

window.platform = {
  name: function () {
    return 'ios';
  },

  foo: function (message) {
    webkit.messageHandlers.handler.postMessage({
      method: 'foo',
      message: message,
    });
  },

  bar: function (message, callback) {
    var uuid = this._uuid();
    this._callbacks[uuid] = callback;
    webkit.messageHandlers.handler.postMessage({
      method: 'bar',
      message: message,
      callback: uuid,
    });
  },

  _callbacks: {},

  _uuid: function () {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
      var r = Math.random() * 16 | 0;
      var v = c == 'x' ? r : (r & 0x3 | 0x8);
      return v.toString(16);
    });
  },

  _invoke: function (uuid, err, data) {
    this._callbacks[uuid](err, data);
    delete this._callbacks[uuid];
  },
};

Here webkit.messageHandlers.handler object is introduced by addScriptMessageHandler method of WKUserContentController class and posted messages are received by ScriptMessageHandler class.

And it is injected by ViewController like this:

let platform = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("platform", ofType: "js")!)
self.webView!.evaluateJavaScript(try! String(contentsOfURL: platform), completionHandler: nil)

How callbacks works?

Starter uses the same callback model as node.js, a function with two arguments: err and data. They are typically used like this:

function (err, data) {
  if (err)
    throw err;
  // data is available here
}

As Starter can't provide the function directly to the native code, a unique identifier is generated by the _uuid function of platform object. When native code needs to invoke this callback, it simply calls the _invoke function with the given identifier.

On Android:

this.mWebView.post(new Runnable() {
    @Override
    public void run() {
        JavascriptInterface.this.mWebView.evaluateJavascript("platform._invoke('" + callback + "', null, true);", null);
    }
});

The callback is not invoked on the UI thread (see post method).

And on iOS:

self.viewController.webView!.evaluateJavaScript("platform._invoke('" + callback + "', null, true);", completionHandler: nil)

Mock platform object for development

You have to mock the platform object during development phase in the browser, you can do something like this:

window.platform = window.platform || {
  name: function () {
    return 'www';
  },

  foo: function (message) {
    alert(message);
  },

  bar: function (message, callback) {
    callback(null, confirm(message));
  },
};

As you can see the object is defined only if it doesn't exist (see index.html).

Native projects support viewer mode

Each project can define in its own application manifest a property named StartURL. If this property is defined, the application starts in viewer mode. That allows the application to load this url in the WebView.

See AndroidManifest.xml and Info.plist

The WebView is initialized like this on Android:

String url = "file:///android_asset/www/index.html";
try {
    ApplicationInfo ai = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
    url = (String) ai.metaData.get("StartURL");
}
catch (Exception ex) {
}

this.mWebView.loadUrl(url);

More information on assets directory can be found here.

Once again, things are equivalent on iOS:

if let url = NSBundle.mainBundle().objectForInfoDictionaryKey("StartURL") as? String {
    self.webView!.loadRequest(NSURLRequest(URL: NSURL(string: url)!))
}
else {
    let index = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("index", ofType: "html", inDirectory: "www")!)
    self.webView!.loadFileURL(index, allowingReadAccessToURL: index.URLByDeletingLastPathComponent!)
}

Goals

GNU make goals are defined in Makefile file. Its main purpose is to copy the HTML5 application located in src directory to native projects:

  • On Android the application is copied to platforms/android/app/src/main/assets/www
  • And on iOS to platforms/ios/www

If the HTML5 application needs to be bundled with tools like browserify or webpack, it must be done here! Let's say that the Makefile knows both worlds (native and Javascript).

fastlane handles following lifecycle tasks of native projects:

  • Run units tests and UI tests
  • Build application
  • Submit application to store

Good tool or bad tool ? fastlane allows you to manipulate native projects in a uniform way!

Starter provides following lanes for both platforms:

  • test: Runs all the tests
  • compile: Compile the application
  • store: Submit the application

For example to build iOS native project, use fastlane ios compile

Check fastlane files for more information: Appfile, Fastfile.

Jenkins pipeline is defined in Jenkinsfile file. Normally Jenkins pipeline should execute: