Titbits from a life

22 May
0Comments

The technical adventure building a hybrid app

The project was aimed at creating a mobile client for sports social network. Scope was very small: Facebook connect login, changing your and your team mates attendance, adding goals and penalties for ongoing matches. Probably, the most interesting feature was Facebook style background synchronization: once you get on Wi-Fi it would check for updates. The next time you opened the application, information was fresh and interesting.

Decision to make hybrid application came from the customer, so I only had the freedom to choose tools, but not the approach. In recollection, I think, I should have been taken an advantage of PhoneGap and it’s plugins if I had a bit more time. While implementing UI, I tried to make the most advantage of already existing website html & css. However, it didn’t save much effort due to limitations of mobile WebKit described in problems.

Android/Javascript interface

To deliver project quickly I dropped PhoneGap and sticked only with technologies I know. One of new things was to use HTML5 mobile boilerplate, which saved some time organizing the build and working around WebKit incompatibilities across platforms. Eventually, I was building a HTML/CSS UI and testing it in regular browser before any Android deployment.

Interface between Android and JavaScript was simple. WebView together with a progress spinner was part of the layout. In Activity.onCreate() or Fragment.onCreateView() I would setup WebView and load html files from assets folder. See code for WebView helper class below:

public class WebViewHelper {

	public static void setup(final WebView webView, Context context, Object obj, final String tag, String filename, final PageLoadListener listener) {

        webView.getSettings().setAllowFileAccess(true);
        webView.getSettings().setJavaScriptEnabled(true);  

        webView.setWebViewClient(new WebViewCallbacksDispatcher(listener));
        webView.setWebChromeClient(new LiiquChromeClient(tag));

        webView.addJavascriptInterface(obj, "Android");

        final String html = readAssetsFile(context, filename);

        webView.loadDataWithBaseURL("file://", html, "text/html","utf-8", null);

	}

	public static void setup(final WebView webView, Context context, String tag, String filename, final PageLoadListener listener) {
		setup(webView, context, context, tag, filename, listener);
	}

	public static String readAssetsFile(Context context, String filename) {
		final StringBuilder builder = new StringBuilder();

		final AssetManager assets = context.getAssets();

        try {
			final LineNumberReader reader = new LineNumberReader(new InputStreamReader(assets.open(filename)));

			String tmp = null;
			while((tmp = reader.readLine()) != null) {
				builder.append(tmp);
			}

			reader.close();
        } catch (IOException e) {
			Log.d("WebViewHelper", "", e);
		}

		return builder.toString();
	}
}

Probably, the most interesting part in the above code are WebViewCallbacksDispatcher and PageLoadListener. My Activities and Fragments would implement the PageLoadListener to receive callbacks, whenever page finished loading. I would listent for these events and call something like webView.loadUrl(“javascript:splashPage.setTheme(\”%s\”)”) to provide data to the UI. Also, note the overridden shouldOverrideUrlLoading method. Probably, the same plug will be needed for “phone:” and “sms:” links.

public class WebViewCallbacksDispatcher extends WebViewClient {

	private PageLoadListener listener;

	public static interface PageLoadListener {
		public void onPageStarting();
		public void onPageFinished();
	};

	public WebViewCallbacksDispatcher(PageLoadListener listener) {
		this.listener = listener;
	}

	@Override
	public void onPageFinished(WebView view, String url) {
		if (listener == null) {
			return;
		}

		listener.onPageFinished();
	}

	@Override
	public void onPageStarted(WebView view, String url, Bitmap favicon) {
		if (listener == null) {
			return;
		}

		listener.onPageStarting();
	}

	@Override
	public boolean shouldOverrideUrlLoading(WebView view, String url) {
		if (url.startsWith("geo")) {
			return true;
		}

		return super.shouldOverrideUrlLoading(view, url);
	}
}

Build process

To be able to play and debug html/css design in the browser, I came up with such build process on top of mobile boilerplate’s standard dev and prod:

  1. Copy files to development web server for css/js viewing in a browser.
  2. Remove  test data marked using pseudo <[!-- TEST DATA --> ... <[!-- //TEST DATA --> comments
  3. Change all the development server links to files://assets/*.* links (all the stylesheets, JS files and images had to have absolute links).
  4. Copy files to app's project /assets folder

Later to speed things up, I made the project's /assets folder in to a symlink to boilerplace's publish/ directory. Note that I was using special comments for JavaScript loading test data, so that they won't be removed when building boilerplate for production.

Problems and Solutions

Biggest problem is surprise, surprise responsiveness. If you want to show additional button after something is clicked, your user has to wait at least a second. In fact, displaying Android dialog and rendering additional controls there results in better experience, as the dialog frame appears at least twice quicker and user understands that something is happening. So, "hidden" UI controls got moved into separate Android dialogs.

Another "speed-up" tactics to somewhat improve the experience is changing background/adding border to touched elements. Mobile boilerplate currently adds rgba(0,0,0,.7). After writing this, I am going to submit a patch, which adds adds a special "pressed" class to the element instead. Not sure if the patch is accepted, thus the snipplet is below. For the same reason (perceived responsiveness -> better UX), imho it is better not to remove default webkit highlight (leave -webkit-tap-highlight-color unchanged) despite all its ugliness.

MBP.fastButton = function (element, handler, active_class) {
  this.element = element;
  this.handler = handler;
  this.active_class = active_class === undefined ? "pressed" : active_class;

  addEvt(element, "touchstart", this, false);
  addEvt(element, "click", this, false);
};

..

MBP.fastButton.prototype.onClick = function(event) {
	event = event || window.event;
  if (event.stopPropagation) { event.stopPropagation(); }
  this.reset();
  this.handler(event);
  if(event.type == 'touchend') {
    MBP.preventGhostClick(this.startX, this.startY);
  }

var pattern = new RegExp(" ?" + this.active_class, "gi");
this.element.className = this.element.className.replace(pattern, ''); };

MBP.fastButton.prototype.reset = function() {
	rmEvt(this.element, "touchend", this, false);
	rmEvt(document.body, "touchmove", this, false);

var pattern = new RegExp(" ?" + this.active_class, "gi"); this.element.className = this.element.className.replace(pattern, '');
};

Second biggest regret came after Android browser failed rendering borders around divs with rounded corners. See image for the best result I achieved (ugly white background highlight it well). Facebook & others are obviously using images for button backgrounds. UPDATE: after writing this, I have received additional confirmation for this: forget about rounded divs, use images to round courners.

Then input[type="number"] is supported only occasionally and partially. While Android 2.2 browser didn’t support this input type, vanilla Gingerbread (tested on 2.3.3 and 2.3.6) showed proper keyboard (numbers only). Still, despite the enter button was called “Next”, it failed to observe html tabindex (“Next” wasn’t switching between input fields). Moreover, the on some of Android “Next” button wasn’t working at all and users had to press hardware Back button to hide the keyboard. No work-around found.

First screen of the app is probably the most important screen for user convertion. Don’t be naive and make it native. Facebook did! I didn’t and regret it. If you choose to be naive and follow holy HTML5, you will find out that Android doesn’t display WebView in a dialog, when another WebView is in the background. At least on Android 2.3.6. True story! If you want to have login screen in html/css, you have to hack the Facebook Android SDK and put Facebook Connect browser dialog in non-dialog Activity.

Conclusions

As you see, choosing hybrid approach for Android application doesn’t have much work on UI, but only significantly increases it. Even with pre-existing stylesheets, I would develop same interface natively faster. Still, the effort was worth-while. Despite I don’t know, how much css/html/js code could be reused in hybrid iOS app, I am sure I could produce webapp using same artifacts very fast. Friends from biggish mobile agency (150+ people) summarized their experience this way: hybrid approach is expensive for one platform and saves insignificant effort when developing only for iOS & Android. However hybrid is totally worth it when targeting 3+ platforms (Android, iOS, web-app and more). It seems to be true.

Ideas for improvement

Despite in my case the WebView loading is fast enough, I had been thinking to try the following, despite I didn’t yet give it a try:

  1. Use a smaller JS library instead of jQuery
  2. Avoid combining all the js and css into two files. Instead, each hybrid view could have it’s own tiny js and css files with business and UI logic.

Useful resources

 
No comments

Place your comment

Please fill your data and comment below.
Name
Email
Website
Your comment