Blog

Mobile enable your Ruby on Rails site for small screens

Jonathon Horsman Dec 01 15 comments

As mobile devices and small screens accessing web sites become more common place, having your website tailored for these devices is becoming increasingly important.

Fortunately Ruby on Rails makes this reasonably easy. I recently went through the process of mobile-enabling our www.arctickiwi.com Ruby on Rails site.

As you can see it’s a simple site, a few static pages with a bit of database-driven content (the blog and recent tweets on the sidebar).

Here’s the process I followed which is reasonably straight-forward and took about 1 day.

The key is detecting the client (browser) connecting to the server and rendering the appropriate layout. For simplicity we have just 2 layouts, one for mobile and one for regular browsers.

The beauty of Ruby on Rails is the layout applies across our entire site, so I just make my changes in one place and they flow across all pages.
The content remains the same on each site and we can control the presentation in the CSS. This is where the true benefit of the MVC layers comes into play, and the advantage of defining all styles in the CSS (rather than inline).

The steps are:

  1. Add mobile detection to your controllers.
  2. Have your controller return the appropriate layout.
  3. Duplicate and customise the application.html.erb layout.
  4. Duplicate and customise the css file – this is the most time consuming part.
  5. Resize large images for mobile screens.
  6. Add a link to switch between mobile and “normal” screen sizes.

I have a Google phone with screen dimension of 320px wide by 480px high, so being selfish that’s the screen size I’m targetting. One of my goals was to avoid the annoying horizontal scroll bar.

First is mobile detection. Whenver a browser or mobile device connects to a web server it sends a bunch of headers, such as the content type it accepts, language and the type of browser making the connection.

The header parameter we care about for mobile detection is the HTTP_USER_AGENT. When I connect from my browser the value I get is:
@
Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.1.5)
Gecko/20091109 Ubuntu/9.10 (karmic) Firefox/3.5.5
@
p. You should recognise a few meaningful pieces of information here, notably I’m running Firefox 3.5 on Ubuntu Linux 9.10.
Likewise when I connect from my Google Android (an HTC Magic branded phone) the header is:
@
Mozilla/5.0 (Linux; U; Android 1.6; en-gb; HTC Magic Build/DRC92)
AppleWebKit/528.5+ (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1
@
p. Apple weenies connecting from an iPhone supply a user agent string which looks like this:
@
Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_1_2 like Mac OS X; en-us)
AppleWebKit/528.18 (KHTML, like Gecko) Mobile/7D11
@
p. Since there’s no standard way of identifying a mobile device (and the definition is vague and constantly changing as new technology is released), I just use a known array of mobile user agents from here.

All our controllers extend from one ApplicationController, which in turn extends from ActionController::Base.
Being the parent class to all other controllers means we can define the browser detection and return the appropriate layout in one place.

Using the HTTP_USER_AGENT in the request headers we do a pattern match array of mobile user agents.
@ ruby
class ApplicationController < ActionController::Base
helper :all
protect_from_forgery
layout :detect_browser

private MOBILE_BROWSERS = [“android”, “ipod”, “opera mini”, “blackberry”, “palm”,“hiptop”,“avantgo”,“plucker”, “xiino”,“blazer”,“elaine”, “windows ce; ppc;”, “windows ce; smartphone;”,“windows ce; iemobile”, “up.browser”,“up.link”,“mmp”,“symbian”,“smartphone”, “midp”,“wap”,“vodafone”,“o2”,“pocket”,“kindle”, “mobile”,“pda”,“psp”,“treo”] def detect_browser agent = request.headers[“HTTP_USER_AGENT”].downcase MOBILE_BROWSERS.each do |m| return “mobile_application” if agent.match(m) end return “application” end

@
p. Notice the layout is simply a method pointer to detect_browser which does a pattern match on each item in my array of mobile user agents.

If any user agent string matches we’ll render the mobile_application layout, otherwise just the usual application layout.

So in my app/views/layouts I copy application.html.erb to mobile_application.html.erb and make appropriate modifications.

The first thing I did was replace stylesheet_link_tag "main" with stylesheet_link_tag "mobile" and copy public/stylesheets/main.css to public/stylesheets/mobile.css

Since your application has a lovely clean separation of layout to content, you only need to make changes to your style sheet, right? If you’ve embedded styling elements in your HTML then you may have some work to do moving them into a stylesheet first.

Our site has a 900 pixel wide banner image which clearly would not fit well in a mobile browser. So I created a public/images/mobile directory and resized all the big images and stuck them in there.
Then in my mobile.css and mobile_application.html.erb files I linked these new, smaller images.

Next is the tedious process of going through your mobile.css stylesheet and tayloring it for small screens.
What I did was remove the boxes which float right (tweets and client testimonials). These I put inline below the main page content.
Likewise the copyright notice and client login button which sit at the top right I moved down to the bottom of the page.
I also squished up the menu, removing excess padding and any fixed width content. I prefer to let my browser render the content to fill the available space, the way HTML was intended.

This fiddling with CSS and moving content around is the most time consuming part. And not being a designer it takes me a while. Fortunately our site is small and the layout is quite clean, with most changes being made just in the CSS and layout file.

The final, optional step is allowing the user to override your browser detection. We may have incorrectly detected their user agent so giving them the option to switch layout is a nice feature.

Sites like Google and Wikipedia use a different URL for their mobile site but I decided to store a variable in the session if the user chooses to override the automatic browser detection. If you have a strong opinion on which is a better method I’d be interested to hear why.

So in the footer bar of my site application.html.erb file I added the following:
@ rhtml
<%= link_to “MOBILE SITE”, :controller => “home”, :action => “set_layout”, :mobile => “1” >
@
p. and likewise in the mobile_application.html.erb file I added:
@ rhtml
<
= link_to “REGULAR SITE”, :controller => “home”, :action => “set_layout”, :mobile => “0” %>
@
p. I then modified the home_controller.rb file to stick the chosen layout in the session:
@ ruby
def set_layout
session[“layout”] = (params[:mobile] == “1” ? “mobile” : “normal”)
redirect_to :action => “index”
end
@
p. And finally I amend detect_browser in the application_controller.rb, having the session variable takes precendence over my auto-detection:
@ ruby
class ApplicationController < ActionController::Base
helper :all
protect_from_forgery
layout :detect_browser

private MOBILE_BROWSERS = [“android”, “ipod”, “opera mini”, “blackberry”, “palm”,“hiptop”,“avantgo”,“plucker”, “xiino”,“blazer”,“elaine”, “windows ce; ppc;”, “windows ce; smartphone;”,“windows ce; iemobile”, “up.browser”,“up.link”,“mmp”,“symbian”,“smartphone”, “midp”,“wap”,“vodafone”,“o2”,“pocket”,“kindle”, “mobile”,“pda”,“psp”,“treo”] def detect_browser layout = selected_layout return layout if layout agent = request.headers[“HTTP_USER_AGENT”].downcase MOBILE_BROWSERS.each do |m| return “mobile_application” if agent.match(m) end return “application” end def selected_layout session.inspect # force session load if session.has_key? “layout” return (session[“layout”] == “mobile”) ? “mobile_application” : “application” end return nil end

end
@
p. As an interesting aside I have to force the session to be loaded with session.inspect otherwise session.has_key? always returns false, since it’s lazy loaded.
If there’s a better way to do this please add a comment and let me know.

I tested this using the Google Android emulator which you can download to trial various screen sizes without having to actually use the hardware.

Comments //

Franklin Lyons

Franklin Lyons Dec 03

Super smart - thank you for putting this out there for us.
My app needed a tweak so I wanted to put it here.
I had to change:

agent = request.headers["HTTP_USER_AGENT"].downcase

to:

agent = request.env["HTTP_USER_AGENT"].downcase

Amir

Amir Jan 05

What if you wanted to make other mobile html views for different parts of the site, not just the application.html.erb. Would I edit that specific controller with the same code below:
  def detect_browser
    layout = selected_layout
    return layout if layout
    agent = request.headers["HTTP_USER_AGENT"].downcase
    MOBILE_BROWSERS.each do |m|
      return "mobile_application" if agent.match(m)
    end
    return "application"
  end

Jonathon Horsman

Jonathon Horsman Jan 20

Hi Amir

To render different layouts in different parts of the site you could just override the detect_browser method in the specific controller.

It would be a good idea to abstract the browser detection logic so that it's not repeated in each controller. So instead of

return "mobile_application" if agent.match(m)

you could use

return mobile_layout() if agent.match(m)

then write a mobile_layout method in the application_controller, e.g.

def mobile_layout
  return "mobile_application"
end

and subclass that in the controllers which you want to render a different layout.

Hope this helps

Jared White

Jared White Apr 01

This is great stuff! Thanks!!

(BTW, you might want to add "iphone" to your array in the sample code, because after mentioning it you don't actually have it in the MOBILE_BROWSERS array...)

Jared White

Jared White Apr 01

This is great stuff! Thanks!!

(BTW, you might want to add "iphone" to your array in the sample code, because after mentioning it you don't actually have it in the MOBILE_BROWSERS array...)

Tom

Tom Apr 08

Great article, thanks! In the code above, is session["layout"] ever set to something? Or should that be done inside selected_layout?

Everett Considine

Everett Considine May 01

btw...I went to view your site on my iphone and got your regulars site.

Jonathon Horsman

Jonathon Horsman May 04

I deliberately omitted iphone because it seemed to render regular sites well enough, but yes if you wanted a mobile-specific site for iphone then adding that would make sense.

Tom: session["layout"] is set in the home controller, but you could put that in application controller.

Andy

Andy May 09

Thanks so much - I used your blog for a starting list of mobile browser search strings. Very helpful :)

Dean

Dean May 30

great every think is good.

Lunn Ponlork

Lunn Ponlork Jun 06

HI,

When i using: <%= @content_for_layout %> in mobile_application.html.erb for call view (mobile_application). it not working with detect_browser. so it going to website normal, not for phone.

what's the problem?

Stephan Wehner

Stephan Wehner Jul 22

You ask "If there’s a better way to do this please add a comment and let me know."

Original method:

def selected_layout
    session.inspect # force session load
    if session.has_key? "layout"
      return (session["layout"] == "mobile") ?
        "mobile_application" : "application"
    end
    return nil
  end

Suggested method

def selected_layout
  session[:layout]
end

This will work with this new set_layout method:

def set_layout
 session["layout"] = (params[:mobile] == "1" ? "mobile" : "application") # was "normal" instead of "application"
 redirect_to :action => "index"
end

Cheers,

Stephan

mobileappdevelopment

mobileappdevelopment Jan 19

thanks alot i liked your blog it helped me a lot in developing my application thanks a lot buddy

mobileappdevelopment

mobileappdevelopment Jan 19

thanks alot i liked your blog it helped me a lot in developing my application thanks a lot buddy

mobileappdevelopment

mobileappdevelopment Jan 19

thanks alot i liked your blog it helped me a lot in developing my application thanks a lot buddy

Adda

Adda Jan 29

I tried following these instructions and when I reload my root I get the following error:

SyntaxError in AlertsController#index

/Users/abirnir/Sites/transpo-hub/app/controllers/application_controller.rb:4: syntax error, unexpected tIDENTIFIER, expecting kDO or '{' or '('
..., “ipod”, “opera mini”, “blackberry”, “palm”...

and it goes on an on...

Any thoughts as to why this is happening?

The tutorial is great, thank you!

penisboy

penisboy Mar 28

ah tanka you for dis wan

Post a comment

  1. optional

Recent Tweets

Blog: The monumental Myspace cock-up: http://bit.ly/emgRKV
Tweeted on Friday at 09:43

Awww railsapi, delete some logs: http://bit.ly/htBNDH
Tweeted on Wednesday at 16:15