Showing posts with label phonegap. Show all posts
Showing posts with label phonegap. Show all posts

Saturday, March 24, 2012

Working around Android Webkit

I use PhoneGap to output the Android app for EatDifferent, and that means that my app runs inside an embedded Android browser. As I've discovered and re-discover everytime I work on a new version of the app, I am not the biggest fan of the Android browser. And that's an understatement.

Sure, the Android browser is Webkit-based, so it technically supports modern HTML5 elements and CSS3 rules, but in practice, the browser can sometimes struggle with rendering the new shiny CSS stuff, especially when some user interaction causes it to repaint the DOM. It's not just that the browser slows down — it actually fails to re-paint DOM nodes (or as I like to describe it, it "white outs" those nodes). When the whited-out nodes are my navigation menu or form buttons, then my app is rendered basically un-usable. That's a shitty user experience, of course, so as the developer, I want to do whatever I can to make sure that a user doesn't have to experience that.

Unfortunately, these white-outs are difficult to debug. When I run into one, I try to replicate it a few times (so I know what user interactions caused it), and then I start stripping out CSS rules until I can't reliably replicate it anymore. Since the glitches only happen on the device themselves (and not in Chrome, where I usually test CSS changes), I have to re-deploy the app everytime I test a change. Needless to say, it's a slow process. The white-outs are also impossible to programmatically test for, as far as I know, so there's nothing I can add to my test suite to guarantee that changes in my code haven't brought any back.

So, yeah, they suck. But they suck less when you know what to look for and what to change, so here are some of the changes I made to out the white-outs.

But first... detecting Android

I use the same HTML, CSS, and JS codebase for both the Android and iOS versions of my app, and for most of the changes, I only wanted to make them for Android. To do that, I have a function that detects if we're on Android or if we're testing Android mode on the desktop. I can use the results of function in my initialization code to add a "android" class to the body tag, which I can reference in my CSS.

My Android detection function checks by looking at the user agent (which isn't as simple as just looking for "Android", thanks to HTC and Kindle) and as a backup, looking at the device information served by the PhoneGap API.

Note that my Android detection function only checks if we're on an Android operating system, not if we're specifically in the built-in Android WebKit browser. My app only needs to check if it's on an Android OS, since it's wrapped inside PhoneGap and not accessed from arbitrary mobile browsers. If you're writing a website accessible from a URL and want to employ these workarounds only on the built-in Android Webkit browser, then you need a check that looks for the Android OS and a non-Chrome Webkit browser. Thanks to Brendan Eich for pointing that out in the comments.

  function inUserAgent(str) {
    return (new RegExp(str)).test(navigator.userAgent.toLowerCase());
  }

  function isAndroid() {

    function isAndroidOS() {
      return inUserAgent('android') || inUserAgent('htc_') || inUserAgent('silk/');
    }

    function isAndroidPG() {
      return (window.device && window.device.platform && window.device.platform == 'Android' || false;
    }

    return isAndroidOS() || isAndroidPG();
  }

  if (isAndroid() || getUrlParam('os') == 'android') {
    $('body').addClass('android');
  }

The case of the shiny modals

I use the modals from Twitter Bootstrap for dialogs in the mobile app, and I noticed white-outs and other visual oddities would happen when a modal rendered. To workaround that, I overrode the modal CSS rules to reset various CSS3 properties to their defaults so that the browser processes them as never being set at all.

.android {
  .modal {
    @include box-shadow(none);
    @include background-clip(border-box);
    @include border-radius(0px);
    border: 1px solid black;
  }
}

After making that change, I decided to go ahead and strip down the Bootstrap buttons, nav bar, and tabs as well — not because I necessarily knew their CSS3 rules were causing issues, but because I'd rather have a useable app than a perfectly rounded, shadowed app. I later realized that the stripped down CSS conveniently matches the new Android design guidelines quite well, so my changes are actually a performance and usability gain. You can see all my Android CSS overrides in this gist, and see the visual effect of the overrides in the screenshot below.


The case of too many nodes

When my app loads the stream, it appends many DOM nodes for each of the updates, and those DOM nodes can include text, links, buttons, and images. Android was frequently whiting out while trying to render the stream, understandably. I made a few changes to improve the performance of the stream rendering:

  • Stripping the CSS3 rules from the buttons (as described above) plus a few other classes.
  • Implementing delayed image loading. I had already implemented that for the web version of the app, since it is pretty silly from a performance and bandwidth perspective to load images that your users may not scroll down to see, and I discussed that in detail in this post.
  • Pre-compiling my Handlebars templates. This is a change I actually made for the iPhone, after discovering super slow compile times on iOS 5, but it helps a bit on Android as well.

The case of the resizing textarea

My app includes a textarea for the user to enter notes which defaults to a few rows. Sometimes users get wordy though and type beyond the textarea, and I wanted the textarea to resize as they typed. I got this plugin working, but I kept seeing my app's header and footer white out when the textarea resized. I eventually figured out that Android didn't like repainting the header and footer because they were position:fixed (it didn't white out when I made them absolute), and couldn't figure out how to get Android to not white them out. So, I opted here to just make sure the code never resized the textarea while the user was typing by adding the resizeOnChange option, and setting that to true for Android. It's not ideal, but well, that's life.

A better future?

As recently announced, there's now a Chrome for Android which is significantly better than the built-in Webkit that ships with it, and I'm hopeful that Android apps will be able to use Chrome for their embedded WebView in the future (see my issue filed on it here). I look forward to making app design decisions based on making a better user experience, and not on preventing a horrible one. :)

Friday, February 17, 2012

Picking a Mobile App Strategy (Slides & Links)

One of the main new technologies that I've been working with this year is PhoneGap, as that's the tool I picked to help me make the mobile apps for EatDifferent. So, this week, in addition to the Client-side Storage talk, I also gave two talks about why I chose PhoneGap and how I'm using it — a short version at Jfokus and a longer version at Valtech.

For both the talks, I wanted to make sure the audience had a clear idea of all the options that are out there for making a mobile app, like using a cross-compiler (ParticleCode) or a bundled runtime engine (Titanium). I spent a day researching those options to make sure that I myself had a good idea of them, and I found that there were many more than I knew about. At a certain point, I just had to stop looking because I feared that the results would be never-ending.

It's a good thing that there are so many tools out there for developers to use, but it also means it's that much harder to know which tool is the right one for you. Plus, these tools are all so new, so there are few developers that have attempted to make real, full-functioning apps using multiple tools, which makes it hard to compare the developer experience across them. It makes me wish we had a Yelp for developer tools — but even still, what works for one developer/app may not work for another.

For the longer talk, I went into some detail on my architecture, workflow, and testing process for my PhoneGap apps (much of what I've detailed in blog posts here), and I did a small demo in front of the audience. I didn't have time to talk about debugging, logging, and offline use this time, but hopefully I can expand that part in my talk at Mix-IT in April.

You can view the slides online for the short talk and the long talk. If you want to research more, you can also browse the links to comparisons and reviews below. Keep in mind that these tools are evolving all the time, so you should always check the current documentation before trusting a reviewer (hoping, of course, that one can actually trust the documentation). If you are embarking down the path of making a mobile app, let us know what you pick and how it works out for you. And good luck!


Links:

Wednesday, February 1, 2012

Delayed Image Loading on Long Pages

On EatDifferent, we let users track their meals with both text and pictures, and now that we have iPhone and Android apps, more and more users are using the mobile apps to quickly snap photos of their meals during the day. That means that the stream of updates on the home page includes a lot of images — a thumbnail for each user updating or commenting, and a thumbnail of each meal (and one user even takes photos of their blood glucose meter before each meal!). Here's a highly scientific diagram of that:

I realized that users were downloading all of these images for the stream, even though many of them were offscreen, and well, I felt bad for unnecessarily using their bandwidth and for making their experience slower. So, I decided to only load the images that were visible (or, nearly visible). Here's how I did it:

The HTML

Normally, when you write an IMG into your HTML, you assign the URL to the src attribute. I didn't want to do that anymore, though, since the browser would then load that URL, so I instead assign the URL to a data-src attribute — something the browser will ignore, but that my JavaScript can pick up. Here's what my template looks like:

<div class="stream-user-img">
 <a href="{{=log.user.profileUrl}}"><img data-src="{{=log.user.photoUrl}}"></a>
</div>

The JavaScript

Now that I have src-less IMG tags, I need a function to decide which IMGs to src-ify. Here's what the function needs to verify:

  • The IMG must have an empty src and a non-empty data-src attribute.
  • The IMG must be visible per CSS - not visibility:hidden or display:none.
  • The IMG must be user-visible, within the viewport. Or, as I realized, it should be nearly visible, so that users don't see the IMGs loading most of the time. I decided on 500 pixels as a reasonable near threshold, just by playing and seeing what felt right.

Here's what that looks like using jQuery:

function loadVisibleImages() {
  $('img').each(function() {
    if (this.src === '' && $(this).attr('data-src') && $(this).is(':visible'))) {
      if (inView($(this), 300)) {
        this.src = $(this).attr('data-src');
      }
    }
   });
}

function inView(elem, nearThreshold) {
  var viewportHeight = getViewportHeight();
  var scrollTop = (document.documentElement.scrollTop ?
          document.documentElement.scrollTop :
          document.body.scrollTop);
  var elemTop    = elem.offset().top;
  var elemHeight = elem.height();
  nearThreshold = nearThreshold || 0;
  if ((scrollTop + viewportHeight + nearThreshold) > (elemTop + elemHeight)) {
    return true;
  }
  return false;
}

Okay, now that we have that function, we need to call it at the appropriate times. It should be called when:

  • New IMG tags are written into the page (after data loading and template rendering, for example).
  • IMG tags go from hidden to visible.
  • IMG tags come into view after a user scroll.

I didn't find an easy way of programmatically detecting those first two cases, so I simply call loadVisibleImages() after my template rendering and visibility toggling (which only happens in two places), and then for the last case, I need to listen to the window scroll event. Unfortunately, I can't simply attach my callback to that event, as some browsers will fire it a lot and executing code after each event will slow down the page scrolling. Instead, I use Ben Alman's throttle library to only call my function every 500 milliseconds during the scroll event. Here's what that looks like:

$(window).on('scroll', $.throttle(500, loadVisibleImages));

The Result

After implementing this viewport-based image loading, my home stream went from loading 120 images to loading just 12 images — a much better experience for the browser and users. There are various improvements that could be made to this technique, like setting appropriate filler images for the different image types (a blank user pic for thumbnails, a meal icon for meals, etc) and maybe even using data URIs for those... but this works well enough for now. ☺

Friday, December 23, 2011

Reusing HTML/CSS/JS across Web & Mobile

At this point, there's only one of me working on EatDifferent. That's awesome because it means I get to learn everything about what it means to make a user-facing website, but it also means that I need to be careful about where I spend my time, since it is such a precious resource.

When I started it, EatDifferent was a web-only service, but I soon realized that when it comes to tracking your daily habits, users really want to be able to track on the go, from their mobile device. I realized I needed a real strategy for how I could offer it as a website, mobile-optimized website, and as multi-platform mobile app while keeping them all up-to-date and still having time to iterate on the core functionality. So I decided to find a way to reuse as much of my code across those offerings as possible. Here's how I do it:

Basic setup

Datastore

When a user logins in, I store their authentication in a session cookie. From then on, either my web app or mobile app can make XMLHttpRequests to fetch or save information for the user. In the mobile app, the calls are made over SSL (and in case you're wondering, the cross-domain restrictions aren't applied to files in Phonegap apps so that is not an issue).

HTML

The Flask microframework comes bundled with support for Jinja2 templates (like Django templates but better) so that's what I use for server-side templating in my web app. I mostly use the templates for includes/inheritance and not for variable rendering, as then I can keep my logic in JavaScript, making it easier to re-use that logic in the mobile app. For example, log.html extends from a base HTML and includes Jinja2 templates for the log sections.

I wanted to reuse much of the same HTML in my mobile app, so I use the Jinja2 template engine for it as well. My mobile app is actually one single HTML page, where each "page" is a DIV with a .mobile-page class, and many of the "pages" include Jinja2 templates (the same ones that are used by the web app). After I make changes to the base HTML or templates, I test them in a browser (using a Chrome extension to mimic a small screen), and then when I want to output them to a device, I render the templates using a script and copy them to the Android/iOS app folders.

CSS

I start off with Twitter Bootstrap for my CSS, because it makes for a slick but easy-to-customize foundation. I then use SASS for writing my own CSS rules, as I can write cleaner cross-browser CSS that way and can also do things like includes. I define my shared styles and variables in common.scss, and include that in web.scss (for the web app) and phonegap.scss (for the mobile app). I use CSS media queries in both common.scss and web.scss to define rules for smaller screens &emdash; some of those rules apply also to the mobile app, but some just to the web app.

JS

As I mentioned in an earlier post, I recently refactored my JavaScript to make it easier to share logic across the web and mobile app. I now have a shared.js for shared functionality, a web.js for web-specific functionality, and phonegap.js for mobile-specific functionality (makes sense, doesn't it? :). Since I also use different JS libraries across the web and mobile app versions, I use different rules in my Makefile to generate the final compressed JavaScript for the web and mobile apps.

Summary

Here's a Venn diagram that summarizes what's different and what's shared:

And here are screenshots comparing the log on the web versus in the mobile app:

I suspect that my HTML/CSS/JS for the mobile app version will diverge more as I try to make the app conform more to the expectations of mobile users (and iPhone users in particular), but I still like the idea of reusing as much of my code as I can. The less time I spend writing redundant code, the more time I can spend adding features and improving the EatDifferent service for all my users.

Saturday, November 19, 2011

Porting from jQuery to Zepto

In my quest for better performance in my PhoneGap Android app, I finally decided to switch from using jQuery to Zepto, a framework that boasts a similar API with a much smaller footprint and additionally offers mobile specific touch events. Of course, since Zepto is designed to be lighter weight than jQuery, it does not offer all the same methods — and the methods that it does offer sometimes differ in their arguments. This makes sense but it means that porting from jQuery to Zepto requires learning how their differences affect your code, and how you can workaround them.

Since other developers might start going down the porting path too, I thought I'd write a post on the differences I encountered while porting. Because I reuse much of my application code between my Phonegap app and desktop web app and I'm not yet ready to eliminate jQuery in my desktop app, I wanted to port my code over in a way that would mean it would work with either jQuery or Zepto as a base framework. That effectively meant that I tried to keep my code the same, and simply add to the Zepto JS file where needed. I'll explain my ports below and then show my Zepto additions at the end.

This post got a bit long, so here are quick links to the sections:

I spent most of my time porting 3rd party jQuery plugin code, as they tend to use the more esoteric jQuery features, but I also found a few places in my own application code where I was using jQuery functionality that's missing from Zepto:

  • CORS:

    I use CORS (cross-domain XMLHttpRequests) to communicate from my mobile app to my server, and that means setting xhrFields in jQuery.ajax(), something that isn't yet supported in Zepto. Luckily, there's a patch for it, and I applied that. I also discovered a bug with sending empty strings in CORS requests, and I work around that by sending null instead.

  • scrollTop:

    I use jQuery.scrollTop() to reset the scroll in my mobile app on page transition, so I took and Zepto-ified the jQuery implementation.

  • position:

    I use jQuery.position() to calculate where to scroll to in some cases, and Zepto only supports offset(). I checked out the jQuery source code for calculating position and stuck that inside Zepto.

  • non-standard selectors:

    jQuery supports several convenience selectors that aren't defined in the w3 spec and simply special cases them in their code, and these won't work as selectors in Zepto. There are some that correspond to methods - ":first" is .first() in Zepto, and ":last" is last(). There are others with no equivalents, like ":hidden", and ":visible", and I just removed my usage of them or checked display values manually. (I didn't feel like checking the jQuery source for them.)

  • valueless attributes:

    There are times when I set and remove valueless attributes on form elements, like "checked", "readonly", and "disabled". In jQuery, I used .attr('disabled', false). That's apparently been replaced in jQuery 1.7 by .prop('disabled', false). Neither will work in Zepto, however, but removeAttr('disabled') will. Since I want my code working with both, I now have .attr('disabled', false).removeAttr('disabled'), just to be sure.

Porting plugins

I use five jQuery plugins in my mobile app, only one of which I wrote myself, and they took the longest to port. For all of these plugins, I started off by changing the final line in the file from '})(window.jQuery);' to '})(window.jQuery || window.Zepto);' and then hoped for the best.

  • ColorSlider:

    I wrote this plugin to give users a colorful slider (red to green), and the only jQuery function it was missing was outerWidth(), so I wrote one based on the jQuery implementation. In doing so, I discovered that the Zepto width() function includes the padding and border, which jQuery.width() doesn't, so I submitted a patch clarifying that in the Zepto docs.

  • jQuery Templates:

    I was pretty worried about this port, but then found out that the jQuery templates was being deprecated in favor of a non-jQuery dependent library, jsRender/jsViews. That library is in beta and the docs are minimal, but the templating syntax is largely the same, and the author responded quickly to my upgrade question. One thing to note, though: the jsViews library tries to take the global $ variable if it doesn't see jQuery defined, so you have to call $.noConflict() after loading it if you want to use $ with Zepto instead — but you don't want to call that if you are actually using jQuery. Here's what I have after I load my scripts: if (window.Zepto) $.noConflict();

  • timeago:

    This handy library turns timestamps into pretty times like "1 day ago" and also auto refreshes elements with timestamps on an interval. It relies on jQuery.trim, which I just copied from their codebase. Along with the DateInput plugin, it relies on the ability to store objects as data attributes (instead of just strings), and thankfully, there's a data.js in the Zepto codebase that I copied into my Zepto JS to handle that.

  • Twitter Bootstrap modal:

    This is a simple modal library that's designed to work with either jQuery or Ender(a lightweight package library), so it already relied on minimal jQuery features. It does attach functions to $.support, which Zepto doesn't define, so I added a one-liner to define it. It also uses $.proxy, which I copied from the jQuery codebase.

  • jQuery Tools DateInput:

    This was by far the hardest plugin to port, and I did look around briefly to see if I should just switch to a completely different pure JS datepicker library, but I quite like this one so I stuck with with it. First, it uses $.expr, a jQuery object that isn't actually documented and (from what rumors tell me) might actually go away soon. I wasn't actually using the results of that function, so I simply defined the object so that the code would not error out. It also uses jQuery.clone and Event.isDefaultPrevented, which were simple enough to write my own versions of.

    Now we get to the tricky parts - methods that exist in both frameworks but don't have the same interface or behavior. I filed bugs on most of these, but the Zepto team could decide to keep their interface the same for simplicity: Zepto doesn't handle multiple self-closing tags in a creation string the same way as jQuery (Issue 322), Zepto does not assign this to the current iterated object in each() (Issue 295), and Zepto does not include an option for deep copy in the extend() function, and Zepto doesn't handle non-DOM objects in expressions/events (Issue 321, That last one unfortunately required a bit of dirty finagling in the DateInput code itself, as its non obvious how to extend Zepto to support binding and firing events on non-DOM objects.

Summary: What's different

To summarize, the following jQuery functionality is missing from Zepto (and much of it is in the gist below):

  • scrollTop()
  • position()
  • ":first", ":last", ":hidden", ":visible"
  • prop()
  • outerWidth()
  • trim()
  • support()
  • proxy()
  • expr()
  • isDefaultPrevented()

And this functionality differs in behavior/arguments:

  • attr('disabled', false);
  • ajax() CORS
  • width()
  • data()
  • each()
  • extend()

My Zepto modifications

Most of the above issues were resolved by defining functions or objects in Zepto, and this gist shows all of those:

I also made a few tweaks to the core Zepto code, and you can see those in this diff (but not all of them are necessarily successful tweaks, nor are they tested.)

Performance comparison

Since my main point in porting over to Zepto was to improve my loading performance, I celebrated the successful porting by measuring the loading times for my page. I used this timing code and compared the differences between including a minified jQuery 1.7 script tag and a minified Zepto script tag, both for a fresh app install and an app re-launch. On average, switching to Zepto shaved 22% off my total loading time. For the detailed results, check out this spreadsheet.

Was it worth it?

I'm happy that I ported, but it's not a decision to be taken lightly. jQuery alternatives are still in early stages, so if you use them, you have to think of yourself as a beta user, be prepared for issues, be prepared for lack of documentation, and do the responsible thing — report whatever issues or workarounds you find. And on that note, thanks to the Zepto authors for bearing with my barrage of issues over the last few days. :)

Wednesday, October 26, 2011

Logging JS Errors on iOS with PhoneGap

I've spent the last few days getting the EatDifferent PhoneGap app working on an iPhone (an app which previously worked on Android). The hardest part has been learning to debug in the iOS browser, so I thought I'd post on my findings:

  • To view the output of console.log, you must open the XCode console. The iOS browser "Debug console" that most iOS debugging articles mention is only displayed in the standalone Safari browser, not in the WebView (where PhoneGap HTML lives).
  • There seem to be times when console.log does not log the output (perhaps during loading?) - in that case, alert() always seems to work.
  • If you log a JS object using console.log, it will just print "Object" by default. You must JSON stringify it to be useful.
  • You can also use debug.phonegap.com (hosted weinre) to view the DOM and JS console logs as well.
  • The WebView browser silently fails on JS errors - it stops running the JS code and does not report the error. To see the error, you must wrap the offending code in a try/catch block.

Given all of those learnings, here is my log() wrapper function that I use across my webapp:

    function log(something) { 
        if (window.console){ 
          if (something instanceof Date) { 
            something = something.toDateString(); 
          } 
          if (isIOS() || isAndroid()) { 
            if (typeof something == 'object') { 
              something = JSON.stringify(something); 
            } 
            console.log(something); 
          } else { 
            console.log(something); 
          } 
        } 
    } 

And I wrap various code blocks in try/catch, like the callback function for AJAX requests:

    try { 
      onSuccess(processJSON(responseJSON)); 
    } catch(e) { 
      log(e); 
    } 

I posted my observations in the PhoneGap group and the developers there made several recommendations: 1) use Ripple, a Chrome extension for mobile emulation 2) monkey-patch JS functions to always try-catch, as done in this library. I've taken a break from iOS debugging for a few days, but I'll probably revisit debugging soon and try out their ideas.

Friday, October 21, 2011

PhoneGap Loading Performance in iOS

As I wrote about earlier, I've been working on the performance of my PhoneGap app, implementing many of the suggestions from this article.

One of the suggestions that article makes is to switch frameworks from something heavy like jQuery (which includes a lot of extra code that may not be necessary on mobile browsers) to something lighter like Zepto or XUI.

I currently use jQuery in my app -- not because I use jQuery a lot in my own code, but because I use a handful of third-party-written jQuery plugins. I could easily port over my own code to a new framework but I don't know how easy it would be to port over someone else's.

So, before I looked into porting from jQuery, I wanted to figure out exactly what effect jQuery had on performance. I'm most concerned with user-facing loading latency versus already-running performance as that's where I perceive the greatest latency in my app, so I decided to see how long it took for the browser to load my HTML, CSS, and JS.

First I wrote a basic function to record times of events and I used it to record events after the CSS, HTML, jQuery script tag, other script tag, document ready, and execution of my setup function This is a stripped down version of what it looked like:

Then I recorded the times in Chrome, iPhone Simulator, and actual iPhone (on the same WIFI network), and graphed them in a spreadsheet. Here's a chart of the results:

As you can see from the chart, the iPhone browser is noticeably slower than Chrome and the simulator, and the jQuery script tag (90 KB) takes 35% of the loading time - same as the custom script tag (220KB). But it's only 300ms, which isn't as slow as I expected and for now, it doesn't seem worth it for me to try to port away from jQuery.

Update (11/20/2011): I have since ported away from jQuery and written that up in this post..

Update (12/13/2011): It's been pointed out that when it comes to measuring the "loading times" for resources, this is not the most precise technique, as it it doesn't reveal the difference between the download time, the parsing time, and the execution time. (In the case of jQuery, I believe much of the time I recorded came from the execution.) For that breakdown, I've been recommended weinre.

Thursday, September 8, 2011

Switching from jQuery Mobile to Twitter Bootstrap

I've been using jQuery mobile (jQM) to make a mobile-enhanced version of EatDifferent for the past few weeks and though I have much respect for the team behind it, I've grown increasingly frustrated with it. Why?

  1. It typically enhances by adding additional DOM elements to the original elements - which can help in making them easier to use on a mobile device, but it means that whenever you want to tweak the styles of an enhanced element, you need to dig into the DOM and apply CSS overrides at various levels. If the DOM or inner CSS names change in later jQM versions, you may have to update those CSS overrides.
  2. If you dynamically update an enhanced DOM element (like a form input, for example), you often have to tell jQM to force refresh that element, since it needs to redo the added DOM. That means you need jQM-specific JavaScript calls in your code.

I was fine with those issues when I used jQM for an earlier project, but for the EatDifferent app, they are becoming bigger issues because: 1) I'm doing more customization, and 2) I'm doing more dynamic form creation and updating.

And most importantly, I'm trying to re-use my HTML/CSS/JS across both my the desktop web version and mobile app, and I want minimal difference between them - both so users have a consistent user experience across them, and so that I can write the code just once.

So today I scrapped jQM and decided to use just HTML/CSS. And since I suck at making pretty CSS, I'm using Twitter's bootstrap library. It's pure CSS (no JS!), it's simple to use, and it's pretty. It's not specifically designed for mobile, but it works well enough on it and is easy to customize when it doesn't quite work.

Here's what the app looked like using jQuery mobile and the default black theme:

Here's what it looks like using Twitter bootstrap and some basic customization:

As you can see, jQuery mobile does create more mobile-optimized form input interfaces (larger areas for clicking, e.g.), but Twitter bootstrap creates clean CSS that I can easily add my own mobile-optimized CSS on top of when necessary. I'm happy I made the switch.