Sunday, April 18, 2010

Optimizing GWT for iPhone

I have been working sporadically on my math quiz application for the past several weeks. To complicate matters beyond just learning GWT, I decided to optimize the user interface for the iPhone. That way my daughter could practice her math on the go.

First of all, here is the current version of the application, deployed on Google App Engine:

http://1.latest.mookiemath.appspot.com/

Notice a few things:

  1. After you login, the application shows you the quiz screen directly.
  2. You can click or tap on the buttons at the bottom of the screen to answer the problems.
  3. If you have a keyboard, you can also use the corresponding number keys (capturing the browser key press events is for another post)
  4. After you complete six problems, the application saves the result to the server.
  5. You can click on the little door icon to switch to a history view that lists your quiz results.

Overall, a very minimalist interface designed for a small screen.

Once I mastered how to apply styles to widgets, there were only two specific tweaks I had to make for the iPhone:

Scaling

Since this is a full-screen application rather than a web page, it needs to disable scaling by adding this meta tag to host page header:

<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0;">

Scrolling

The history view consists of a fixed header and a scrollable body:

  <g:DockLayoutPanel unit="EM">
    <g:north size="3.2">
      <g:FlowPanel>
        <g:Label ui:field="iphoneHelp">
          Use two fingers to scroll
        </g:Label>
        <g:Button ui:field="quizButton" text="Quiz"/>
      </g:FlowPanel>
    </g:north>
    <g:center>
      <g:ScrollPanel>
        <g:FlowPanel ui:field="history" />
      </g:ScrollPanel>
    </g:center>
  </g:DockLayoutPanel>

A ScrollPanel is basically just a DIV with the CSS overflow property set to auto. This works fine in a full-sized browser, but mobile Safari does not respect the auto setting by showing a scrollbar when the content overflows. Instead, you have to use a two-finger gesture to scroll the content.

I didn't know this until today, and I expect that many other people do not either, so I wanted to add a simple help label that would only appear for iPhone users. GWT provides "deferred binding" as a general technique for substituting client classes (including widgets) based on user agent or other properties, but that seemed like overkill for this one label. Instead I just relied on a simple native method in my HistoryPanel widget to retrieve the user agent and hide the help label for non-iPhone users:

public class HistoryPanel extends Composite {

  ...

  @UiField
  Label iphoneHelp;

  public HistoryPanel(final HandlerManager handlerManager) {

    DockLayoutPanel parent = uiBinder.createAndBindUi(this);
    initWidget(parent);

    if (getUserAgent().indexOf("iphone") == -1) {
      iphoneHelp.removeFromParent();
    }

    ...
  }

  private native String getUserAgent() /*-{
    return navigator.userAgent.toLowerCase();
  }-*/;

  ...
}

Other than those two issues, the layout looks surprisingly consistent on full-sized browsers and the iPhone.