So, you have this great web app and you’re targeting mobile users (for our purposes, this means Android, iOS, and anything small/hand-held with a full browser and a touch screen). How do you make sure your app is usable on small, hand-held devices?
There are a number of interface strategies, including:
- Design only for mobile, and particularly the phone (iPhone/Android phone only).
- Design for desktop only, and assume mobile users will zoom as needed.
- Design separately for mobile and desktop, and figure out which interface to use somehow.
and there are permutations here based on whether you want to have a different experience on tablets versus phones, tablets versus desktops, Android vs. iOS vs. Blackberry, and so on.
For our system, we’re starting small – designing for iPhone – and scaling up to the tablet and desktop as we go. This will allow us to make sure the interface really works when all you have is your phone. We’ll test the app on both the iOS simulator and the Android SDK simulator on several versions of Android and the two latest iOS, and again in various Desktop browsers (Safari, Chrome, Firefox, and some reasonably intelligent version of Internet Explorer – just as soon as Microsoft releases one).
For our initial purposes, we need different layouts for both desktop and mobile. So we need to figure out how to serve mobile differently. Again, there are a couple different strategies:
- User-agent testing on the server-side (check HTTP_USER_AGENT or its equivalent).
- User-agent testing on the client-side, via JavaScript and DOM manipulation.
- User-agent testing on the client-side, via specialized CSS calls. A List Apart has a great explanation of how this is done.
#2 and #3 both seem overly complicated to me. For now, we’re going with sniffing the user-agent string. The strategy for implementing this in Rails is to do something like this:
# app/controllers/application_controller.rb before_filter :check_layout def check_layout case params["layout"] when "mobile" then session[:layout] = "mobile" when "full" then session[:layout] = "full" when nil then session[:layout] = ua_is_mobile ? "mobile" : "full" else session[:layout] = "full" end end def ua_is_mobile uastring = request.env["HTTP_USER_AGENT"].downcase rescue nil # you could check several patterns here return true if uastring =~ /mobile/ return false end
and then you can either use layout :mobile or the default layout depending on the value of session[:layout]. In our case we have separate layout files entirely, which reduces the number of if statements in the layout. We can then DRY up the layouts using shared partials and helpers. I also seek to minimize repetition in the views by having view snippets be device agnostic, and leaving it to CSS to make things look right for mobile.
Alright then, so that works just fine, but how do we test it? A major problem we run into is in making sure the right layout is rendered for the right user agent. You have to adjust your test to pass in the right user-agent string:
# test/functional/users_controller_test.rb
test "should detect mobile" do
@request.env["HTTP_USER_AGENT"] = "Mozilla/5.0 " +
"(iPhone; U; CPU like Mac OS X; en) AppleWebKit/420" +
"+ (KHTML, like Gecko) Version/3.0 Mobile/1A543a " +
"Safari/419.3"
get :index
assert_template :layout => "application.mobile"
end
which will assert the appropriate layout is used. See the description of assert_template for more goodies.