Working with mobile web applications
So for the past year, I’ve been working almost exclusively on a mobile web application that is designed to run on Android and iPhone. It works quite well on Windows Phone (Mango) as well, but none of our customers have WP devices (yet).
The application is a LOB app with offline capabilitites, so it’s mostly a standard CRUD app. While developing this application, I’ve learned a lot about the different quirks in the browsers on Android and iPhone, and this is what this blog post is going to be about.
Application Cache
Using the application cache is a must for anyone who wants to develop a web app with offline capabilities. At first we ignored the application cache, because we just couldn’t get it working. The problem was that as soon as the application cache was updated, it was impossible to send a request to the server. Turns out that we just didn’t read the spec, and we were missing:
NETWORK: *
in the manifest.
User specific cache
To use the application cache, we needed to cache views depending on which user was logged in. So setting:
<html manifest="/Manifest">
on our start page was out of the question, since that would mean the browser would download the manifest even if you’re not logged in. Instead, we decided to use an iframe which loads once you have logged in. The contents of the iframe looks something like this:
<!DOCTYPE html>
<html manifest="/Manifest">
<head>
<script type="text/javascript">
var app_cache = window.parent.jQuery(applicationCache);
app_cache.bind("cached checking downloading error noupdate obsolete progress updateready", function(event) {
window.parent.ApplicationCacheEventDispatcher.trigger(event, applicationCache);
});
</script>
</head>
<body>
</body>
</html>
So when you log in, we use Javascript to append an iframe pointing to that html file. The Javascript in the iframe makes sure that any application cache event that is caught in the iframe gets passed on to the main window, which lets our application display a loading message to the user.
Just don’t forget to print out the user id or something unique for the logged in user in the manifest, otherwise the application cache won’t be updated if the user logs out, and logs in with a different account. Our solution to this is to print out the user id and application version as a comment in the manifest, like this:
CACHE MANIFEST # version: @Version # user: @User.Id CACHE: / [...]
Forcing an upgrade
Sometimes you need to make breaking changes between versions, and you need to make sure that the application cache is updated on all devices before your users can continue to use the application.
An issue we ran into quite a lot at first was that after a customer had upgraded the application on the server, random errors occured for their users. This happened because the users still had the old Javascript files. That is, the application cache wasn’t updated fast enough.
To get around this, we started to pass the version number along with every request to the server. That gave us the option to check the version that came with the request, compare it to the server version, and tell the client to update the application cache if necessary.
HTTP status == 0
A couple of weeks ago, a bug report came in where a user was using the offline mode in the application. He went to another site, then came back to the application only to find it unusable. None of the pages would load from the application cache. Turns out that by browsing to another site and then returning and trying to fetch something from the application cache can result in getting a HTTP status of 0 instead of 200 in Android. And because of the incorrect HTTP status, jQuery reported it (correctly) as a failed request. So we needed to change the code to take this into account. A somewhat simplified version:
function get(url, on_success, on_error) {
$.get(url, {},
function succ(response, textStatus, XHR) {
on_success && on_success(response, textStatus, XHR);
},
function err(jqXHR) {
if (jqXHR.status === 0) {
if (jqXHR.responseText.trim() == "") {
on_error && on_error(jqXHR);
} else {
on_success && on_success(jqXHR.responseText, "ok", jqXHR);
}
} else {
on_error && on_error(jqXHR);
}
}
);
}
localStorage
LocalStorage works mostly as you expect it; it’s a simple key-value-store with a finite amount of storage, and once you hit that you get an exception. But we got a really wierd bug report from a user where he would browse to another site, and come back to the application to find that all of his settings and data were gone.
Turns out that under some circumstances, Android doesn’t load the localStorage data when you hit the back button. I’m guessing it has something to do with the fact that Android doesn’t always reload the page when you go back, it loads it from the browser cache (not the application cache) instead.
So to get around this Android bug, we set a persistent value in localStorage which we use as a check to see if localStorage is correctly loaded.
var check = "1234567890"
localStorage.setItem("android_check", check);
and when we want to fetch something from localStorage, we do:
var hasItem = function() {
return (typeof localStorage[key] != "undefined" && localStorage[key] !== null);
}
var getItem = function(key, def) {
if (!hasItem(key)) {
if (localStorage.getItem("android_check") !== check) {
// Reload the page so Android reloads localStorage
window.location.reload();
}
return def;
}
return localStorage.getItem(key);
}
overflow: scroll
Our application sometimes needs to open a popup menu with different options, and that menu needs to be scrollable. This is simple in iOS5 since it supports overflow:scroll but in Android, not so much. And it’s not just that, there are differences in how you have to solve it for Android 2 and 3. This because you can’t set scrollTop on elements with overflow:hidden in Android 3, but it works for Android 2.
The way we solved it in Android 3 was to set overflow:visible and setting a fixed height on a wrapper element, and then adjusting the menus margin-top in a touchmove event handler. Needless to say, the scrolling experience in Android 3 is horrible compared to iOS5 and Android 2, but at least it works.
Inputs in Android
I don’t know how many hours I’ve spent fiddling with making inputs on Android play nicely. The root of the problem is that Android doesn’t use the browsers built-in input controls, but instead places some other wierd, internal input above the browsers input field.
Setting focus on an input field
The forms in our application works in a quite specific way. Instead of just displaying the input fields directly, we have a display element that is hidden and replaced with the real input when you tap on the wrapping element.
That is, the tap event handler for the wrapping element calls .hide() on the display element, and .show().focus() on the input. The reason we do this is to be able to format the display in a different way than when you edit the value.
The problem with this is that calling .focus() on an input in Android doesn’t bring up the keyboard, unless the call to .focus() happens in a click event handler. This is a minor issue for us, because the tap event handler gets called about half a second before a click event occurs, which gives us time to show the real input, and when the actual click occurs, the input is visible and catches the click, and the keyboard is displayed.
The real problem is if you want to give an input focus when you click somewhere else in the application. It simply doesn’t work, the user has to tap twice for that; once on our custom edit button to show the input, and then once on the input to get the keyboard.
Page animations
When you go from one page to another, the old page is transitioned out to the left, and the new page comes in from the right. While CSS transitions is very much possible in Android, it’s actually a much smoother experience to use jQuery.animate(), espcially if the pages are long. But I hear that Android 4 has hardware accelerated CSS transitions, so I hope to get a better result there.
An issue with page animations is the scroll position when you go from one page to another. When you hit the back button, you want to land on the same scroll position as when you left that page. This just works in Android, the scrolltop is automatically set correctly. But on iPhone, you’re back to the top of that page, which kinda sucks.
The way we’ve solved this is to save the $(window).scrollTop() before a page animation starts, and then when the pages have finished animating, check if there is a saved scrolltop for the current url. If there is one, we set the $(window).scrollTop() to that, but only if $(window).scrollTop() == 0.
About this entry
You’re currently reading "Working with mobile web applications", an entry on The Coffeescripter
- Published:
- 2012.01.25 at 11.38
- Comments:
- 4 Comments
- Category:
- Mobile Web
4 Comments
Jump to comment form | comments rss [?] | trackback uri [?]