Tworzenie stron mobilnych w JQuery Mobile i GWT

Aplikacje oraz strony internetowe tworzone pod urządzenia mobilne zdobywają coraz większą popularność. Wzrastająca liczba urządzeń wraz z zwiększającymi się stale możliwościami umożliwiają projektowanie ładnych, dynamicznych interfejsów użytkownika. Na rosnące zapotrzebowanie odpowiedzią są liczne frameworki ułatwiające tworzenie mobilnych stron i aplikacji. Część z nich przeznaczona jest na pojedyncze platformy (jak iOS), niektóre pozwalają budować aplikacje wieloplatformowe. Sencha Touch, PhoneGap, Rhodes, JQuery Mobile, to tylko niektóre z nich. Niestety zdecydowana większość to biblioteki JavaScript’owe i tylko niewiele z nich się pod tym względem wyróżnia.

Jako że tematyką blogu jest głównie GWT, postanowiłem sprawdzić, czy istnieją już gotowe rozwiązania umożliwiające na pisanie stron mobilnych w Javie. Na pierwszy ogień poszła Sencha Touch, z tego względu iż na co dzień wykorzystuję bibliotekę Ext GWT tej samej firmy, która to wywodzi się z biblioteki Ext JS napisanej w JavaScripcie. Pomyślałem, że może firma zrobi ten sam manewr z Sencha Touch i przepisze swoją JavaScriptową bibliotekę na Javę. Niestety takiej implementacji nie znalazłem, jednak na forum Senchy można zaobserwować rosnący ruch zwolenników GWT czekających na taką wersję. Rokowania na przyszłość są, bieżących rozwiązań nie ma.

W dalszych poszukiwaniach trafiłem na bibliotekę gwt-phonegap,  która okazała się „tylko” wrapperem JavaScriptowego frameworka PhoneGap. Owszem, da się w tym pisać, jednak uparcie postanowiłem szukać rozwiązania natywnego. Dodatkowo biblioteka służy bardziej do pisania aplikacji, niż stron, gdyż jej głównym celem jest implementacja API systemu mobilnego, np. dostępu do listy kontaktów, obsługi kamery, geolokacji, akcelerometru, czy też systemu plików. Jak na razie nie zwiera jeszcze elementów graficznego interfejsu użytkownika, tak więc odpada.

Z dalszych znalezisk warto wspomnieć o gwt-mobile-webkit, ciekawej bibliotece implementującej API HTML5 w zakresie składowania danych oraz geolokacji. Całość została napisana w GWT, jednak aktualna wersja nie posiada praktycznie żadnych graficznych elementów, z których można byłoby stworzyć stronę. Co najgorsze, chyba przestała być rozwijana (ostatnia zmiana w kwietniu 2010), a zapowiadała się na prawdę nieźle.

Tracąc już nadzieje na jakiekolwiek natywne rozwiązanie, zacząłem przyglądać się bibliotece JQuery Mobile, będącej rozszerzeniem popularnego frameworka JQuery. Szczególnie ona spodobała mi się pod względem estetyki elementów graficznych oraz wsparcia przez wiele przeglądarek. Zacząłem budować prostą stronę rozpoczynając od intefejsu, którego stworzenie wymaga jedynie umiejętności posługiwania się HTML’em. Zanim jednak doszedłem do bardziej zaawansowanych funkcji API, kolega podsunął mi dość świeży projekt, nazywający się gwt-jquery-ui. Na dzień dzisiejszy strona projektu nie wygląda zachęcająco, głównie ze względu na brak jakichkolwiek informacji o bibliotece, czy też wydanych już wersjach. Pewne nadzieje stwarzał jednak przygotowany Showcase, więc czym prędzej pobrałem więc źródła projektu, skompilowałem i przystąpiłem do implementacji. Po dość krótkim czasie miałem już zalążek strony napisanej w GWT wyglądającej jakby była stworzona w JQuery Mobile. „Wyglądającej”, bo biblioteka zawiera własną implementację elementów interfejsu użytkownika i korzysta jedynie z plików CSS dostarczanych przez JQuery Mobile. I w dodatku działa!

Wróćmy jednak od początku. Pokażę Ci dziś, jak zbudować prostą stronę mobilną w GWT. Wykorzystamy do tego wspomnianą wyżej bibliotekę, dzięki której jedynym fragmentem html’a napisanym ręcznie, będzie główny plik strony, w dodatku bardzo krótki. Strona będzie wyglądała tak, jakby została stworzona za pomocą JQuery Mobile i będzie zawierała kilka widoków prezentujących różne elementy interfejsu. Zaczynamy!

index.html
<!doctype html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta http-equiv="Content-Language" content="pl-pl" />
    <meta name="viewport" content="width=device-width, minimum-scale=1, maximum-scale=1">

    <title>Sample GWT JQuery-Mobile</title>
    <link type="text/css" rel="stylesheet" href="http://code.jquery.com/mobile/1.0a2/jquery.mobile-1.0a2.min.css"></link>
  </head>

  <body>
    <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe>

    <script type="text/javascript" src="sample/sample.nocache.js"></script>
  </body>
</html>

Jak już wspomniałem, biblioteka wykorzystuje plik CSS z JQuery Mobile, na daną chwilę w wersji 1.0a2, właśnie taki więc wczytujemy. W części Body mamy wczytanie głównego pliku modułu ze skompilowanym kodem JS oraz wbudowaną ramkę umożliwiającą obsługę historii.

Sample.gwt.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 1.6.4//EN" "http://google-web-toolkit.googlecode.com/svn/tags/1.6.4/distro-source/core/src/gwt-module.dtd">
<module rename-to="sample">

  <!-- Inherit the core Web Toolkit stuff.                  -->
  <inherits name="com.google.gwt.user.User"/>
  <inherits name="com.google.gwt.logging.Logging"/>

  <!-- Inherits JQuery library                              -->
  <inherits name="com.google.gwt.jquery.mobile.JQueryMobile"/>

  <set-property name="gwt.logging.popupHandler" value="DISABLED"/>

   <!-- Specify the app entry point class.                   -->
  <entry-point class="pl.avd.samples.gwt.jquery.mobile.client.SampleModule"/>

  <!-- Specify the paths for translatable code                    -->
  <source path='client' />
  <source path='shared' />
</module>

W linii nr 10 wczytujemy moduł implementacji JQuery Mobile w GWT. Reszta nie wymaga dodatkowego komentarza.

Projekt nie będzie posiadał części serwerowej, dane testowe będą tworzone po stronie klienta, jednak nic nie stoi na przeszkodzie, żeby pobierać je poprzez mechanizm RPC. Nie chciałem jednak tego robić, dzięki czemu projekt da się osadzić na zwykłym serwerze WWW. Dalsza część opisu będzie zawierała implementację czterech widoków: Kontakty, Ulubione, Ustawienia, Dane osoby. Widoki będą implementowane częściowo w plikach Javy, a częściowo w plikach XML przy pomocy UiBinder’a. Jeśli nie miałeś jeszcze do czynienia z UiBinder’em, zachęcam do odwiedzenia strony Google’a poświęconej na jego temat. Z powodzeniem można jednak implementować wszystko w kodzie Javy, chcę tu jednak pokazać, że biblioteka gwt-jquery-ui świetnie wykorzystuje wszystkie mechanizmy GWT. Implementacja historii oraz zarządzanie widokiem będzie zawarte w głównym pliku modułu.

SampleModule.java
public class SampleModule implements EntryPoint, ValueChangeHandler<String> {

  public static String THEME = "a";

  private Logger LOG = Logger.getLogger("SAMPLE");

  private BaseView currentView;

  public void onModuleLoad() {
    GWT.setUncaughtExceptionHandler(new GWT.UncaughtExceptionHandler() {

      public void onUncaughtException(Throwable caught) {
        LOG.log(Level.SEVERE, "uncaught exception", caught);
      }
    });

    Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {

      public void execute() {
        init();
      }
    });
  }

  private void init() {
    History.addValueChangeHandler(this);
    JQueryMobile.init();

    doViewChange(History.getToken());
  }

  @Override
  public void onValueChange(ValueChangeEvent<String> event) {
    String token = event.getValue();

    doViewChange(token);
  }

  private void doViewChange(String token) {
    if (currentView != null) {
      RootPanel.get().remove(currentView);
    }
    currentView = createView(token);
    RootPanel.get().add(currentView);
  }

  private BaseView createView(String token) {
    if (token == null) {
      return new ContactsView();
    } else if (token.startsWith("settings")) {
      return new SettingsView();
    } else if (token.startsWith("person-")) {
      String surname = token.substring(token.indexOf("-") + 1);
      Person person = SampleData.get().findPerson(surname);

      if (person != null) {
        return new PersonView(person);
      }
    } else if (token.startsWith("favourites")) {
      return new ContactsView(true);
    }

    return new ContactsView();
  }
}

Sterowanie widokiem odbywa się poprzez mechanizm historii, dzięki czemu będzie można w łatwy sposób tworzyć linki do poszczególnych stron.

Wszystkie widoki dziedziczą po klasie BaseView, która zawiera implementację nagłówka każdej strony. Nagłówek zawiera w sobie menu aplikacji oraz przyciski sterujące historią. Menu będzie takie samo na wszystkich stronach.

BaseView.java
public abstract class BaseView extends Composite {

  Page page = null;
  private Button contactsBtn;
  private Button favouritesBtn;
  private Button settingsBtn;

  private HeaderBar header;
  private NavBar navi;

  protected void init() {
    initHeader();
    initButtons();
    initTheme(page);
  }

  protected void initHeader() {
    header = new HeaderBar();
    header.setText(getText());

    Button prev = new Button("Wstecz", "arrow-l");
    prev.setRounded(true);
    prev.addClickHandler(new ClickHandler() {

      @Override
      public void onClick(ClickEvent event) {
        History.back();
      }
    });
    header.add(prev);

    Button next = new Button("Dalej", "arrow-r");
    next.setRounded(true);
    next.setIconPos("right");
    next.setPosition("right");
    next.addClickHandler(new ClickHandler() {

      @Override
      public void onClick(ClickEvent event) {
        History.forward();
      }
    });
    header.add(next);

    page.insert(header, 0);
  }

  private void initButtons() {
    navi = new NavBar();
    header.add(navi);

    contactsBtn = new Button("Kontakty", "home");
    contactsBtn.setHref("#contacts");
    navi.add(contactsBtn);

    favouritesBtn = new Button("Ulubione", "star");
    favouritesBtn.setHref("#favourites");
    navi.add(favouritesBtn);

    settingsBtn = new Button("Ustawienia", "gear");
    settingsBtn.setHref("#settings");
    navi.add(settingsBtn);
  }

  public void initTheme(Widget w) {
    if (w instanceof HasDataTheme) {
      if (null == w.getStyleName() || w.getStyleName().indexOf("sticky-theme") < 0) {
        ((HasDataTheme) w).setDataTheme(SampleModule.THEME);
      }
    }

    if (w instanceof HasWidgets) {
      Iterator<Widget> iter = ((HasWidgets) w).iterator();
      while (iter.hasNext()) {
        initTheme(iter.next());
      }
    }
  }

  public abstract String getText();
}

Widok listy kontaktów oraz listy ulubionych implementowany jest przez jedną klasę, różnica będzie polegała jedynie na pobieraniu danych.

ContactsView.java
public class ContactsView extends BaseView {

  private static ContactsViewUiBinder uiBinder = GWT.create(ContactsViewUiBinder.class);

  interface ContactsViewUiBinder extends UiBinder<Page, ContactsView> {
  }

  @UiField ListView contacts;

  private boolean onlyFavourites;

  public ContactsView() {
    this(false);
  }

  public ContactsView(boolean onlyFavourites) {
    this.onlyFavourites = onlyFavourites;
    page = uiBinder.createAndBindUi(this);

    initContacts();
    initWidget(page);
    init();
  }

  private void initContacts() {
    char divider = 0;

    for (Person person : SampleData.get().listPersons(onlyFavourites)) {
      char first = person.getSurname().charAt(0);

      if (first != divider) {
        divider = first;

        ListItemDivider lid = new ListItemDivider();
        lid.add(new HTMLPanel("" + divider));
        contacts.add(lid);
      }

      ListButton lb = new ListButton(person.getSurname() + " " + person.getName());
      lb.setHref("#person-" + person.getSurname());

      if (person.isFavourite()) {
        lb.setIcon("star");
      }

      lb.setDataTheme("c");
      lb.addStyleName("sticky-theme");

      contacts.add(lb);
    }
  }

  @Override
  public String getText() {
    return "Kontakty";
  }
}

Założyłem tu, że pobrane dane o osobach są już posortowane według nazwiska. Przy każdej zmianie pierwszej litery w kolejnych nazwiskach dodatkowo do listy osób dokładany jest informacyjny wiersz oddzielający nazwiska zaczynające się na inną literę.

ContactsView.ui.xml
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
  xmlns:g='urn:import:com.google.gwt.user.client.ui' xmlns:jqm='urn:import:com.google.gwt.jquery.mobile.ui'>

  <jqm:Page dataTheme="c">
    <jqm:Content>
      <jqm:ListView ui:field="contacts"/>
    </jqm:Content>
  </jqm:Page>
</ui:UiBinder>

W tym widoku XML zawiera jedynie umiejscowienie listy kontaktów na danej stronie.

PersonView.java
public class PersonView extends BaseView {

  private static PersonViewUiBinder uiBinder = GWT.create(PersonViewUiBinder.class);

  interface PersonViewUiBinder extends UiBinder<Page, PersonView> {
  }

  private Person person;

  @UiField TextBox name;
  @UiField TextBox surname;
  @UiField CheckButton favourite;

  @UiField Button saveBtn;

  public PersonView(Person person) {
    this.person = person;
    page = uiBinder.createAndBindUi(this);

    initForm();
    initWidget(page);
    init();
  }

  private void initForm() {
    name.setValue(person.getName());

    surname.setValue(person.getSurname());

    favourite.setChecked(person.isFavourite());
    favourite.setShowIcon(true);
    favourite.setText("TAK");
    favourite.addStyleName("sticky-theme");

    saveBtn.addClickHandler(new ClickHandler() {

      @Override
      public void onClick(ClickEvent event) {
        person.setName(name.getValue());
        person.setFavourite(favourite.isChecked());
        History.back();
      }
    });
  }

  @Override
  public String getText() {
    return "Osoba: " + person.getName() + " " + person.getSurname();
  }
}

Widok danych osoby zawiera w sobie pola tekstowe Imię i Nazwisko oraz pole wyboru oznaczające, czy osoba należy do ulubionych. Dodatkowo zaimplementowano obsługę przycisku zapisu, która aktualizuje dane osoby oraz przywraca poprzedni widok, w którym użytkownik przebywał.

PersonView.ui.xml
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
  xmlns:g='urn:import:com.google.gwt.user.client.ui' xmlns:jqm='urn:import:com.google.gwt.jquery.mobile.ui'>

  <jqm:Page dataTheme="c">
    <jqm:Content>
      <jqm:FieldContain>
        <g:Label>Imię</g:Label>
        <jqm:TextBox ui:field="name"></jqm:TextBox>
      </jqm:FieldContain>
      <jqm:FieldContain>
        <g:Label>Nazwisko</g:Label>
        <jqm:TextBox ui:field="surname" readOnly="true"></jqm:TextBox>
      </jqm:FieldContain>
      <jqm:FieldContain>
        <g:Label>Ulubiony</g:Label>
        <jqm:CheckButton ui:field="favourite"></jqm:CheckButton>
      </jqm:FieldContain>
    </jqm:Content>

    <jqm:FooterBar>
      <jqm:ControlGroup roundingType="all">
        <jqm:Button ui:field="saveBtn" icon="check" text="Zapisz zmiany"/>
      </jqm:ControlGroup>
    </jqm:FooterBar>
  </jqm:Page>
</ui:UiBinder>

XML zawiera rozmieszczenie poszczególnych pól oraz stopkę, w której umieszczono przycisk zapisu danych.

SettingsView.java
public class SettingsView extends BaseView {

  private static SettingsViewUiBinder uiBinder = GWT.create(SettingsViewUiBinder.class);

  interface SettingsViewUiBinder extends UiBinder<Page, SettingsView> {
  }

  @UiField RadioGroup themeGroup;

  public SettingsView() {
    page = uiBinder.createAndBindUi(this);

    initForm();
    initWidget(page);
    init();
  }

  private void initForm() {
    initThemeButton("a");
    initThemeButton("b");
    initThemeButton("c");
    initThemeButton("d");
    initThemeButton("e");
  }

  private void initThemeButton(final String theme) {
    RadioButton btn = new RadioButton();
    btn.setText("Schemat " + theme.toUpperCase());
    btn.setDataTheme(theme);

    if (SampleModule.THEME.equals(theme)) {
      btn.setChecked(true);
    }

    btn.addValueChangeHandler(new ValueChangeHandler<Boolean>() {

      @Override
      public void onValueChange(ValueChangeEvent<Boolean> event) {
        if (event.getValue()) {
          SampleModule.THEME = theme;
          initTheme(page);
        }
      }
    });
    themeGroup.add(btn);
  }

  @Override
  public String getText() {
    return "Ustawienia";
  }

Widok ustawień zawiera grupę przycisków radio pozwalających na wybór aktualnego schematu graficznego. Biblioteka JQuery Mobile definiuje domyślnie 5 schematów kolorów, w ramach jednej strony elementy mogą korzystać z różnych schematów. Tutaj aktualizujemy schemat dla wszystkich elementów na danej stronie.

SettingsView.ui.xml
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
  xmlns:g='urn:import:com.google.gwt.user.client.ui' xmlns:jqm='urn:import:com.google.gwt.jquery.mobile.ui'>

  <jqm:Page dataTheme="c">
    <jqm:Content>
      <jqm:FieldContain>
        <g:Label>Schemat kolorów</g:Label>
        <jqm:RadioGroup ui:field="themeGroup"/>
      </jqm:FieldContain>
    </jqm:Content>
  </jqm:Page>
</ui:UiBinder>

W pliku XML dla widoku ustawień mamy umiejscowienie grupy przycisków radio wraz z przypisaniem do niej etykiety.

To tyle, jeśli chodzi o samą aplikację. Demo można zobaczyć tutaj. Kod źródłowy znajduje się do pobrania w tej lokalizacji, jednak zachęcam też do skorzystania z repozytorium.

Niewątpliwą zaletą biblioteki gwt-jquery-ui jest implementacja elementów interfejsu przy pomocy GWT, przy czym elementy te silnie korzystają z hierarchi widget’ów GWT’owych. Daje to pewną przewagę nad biblioteką JavaScriptową, gdyż elementy JQM można osadzać wewnątrz paneli GWT, a panele GWT wykorzystywać wewnątrz elementów JQM. Ogromną zaletą jest też możliwość skorzystania z mechanizmu GWT RPC do ładowania niezbędnych danych. Niestety są też pewne wady. Projekt nie do końca jest dokładnym odwzorowaniem JavaScriptu w Javie, przez co niektóre elementy wyglądają nieco inaczej. Nie wszystko zostało też przepisane i pewnych rzeczy po prostu nie da się zrobić. Pozostaje mieć nadzieję, że autor biblioteki nie zapomni o niej i będzie ją sukcesywnie rozwijał. Potencjał ma duży i już dziś pozwala na tworzenie mobilnych stron i aplikacji w GWT!

Skomentuj

6 Komentarze.

  1. Graficznie prezentuje się bardzo ładnie, a fakt, że to wszystko za pomocą GWT otwiera nową drogę temu rozwiązaniu. Teraz można naprawdę szybko napisać małą aplikację, która będzie działać na wielu urządzeniach przenośnych.

  2. Ja tylko najbardziej ubolewam, że style na stronie napisanej w JQuery Mobile trochę się „krzaczą” na Operze :( Nie są to jakieś bardzo rażące ubytki i może da się to jakoś poprawić.

  3. Fajny artykuł opisujący różnice między ukochanymi Framework’ami. Jednakże gdy potrzebujemy czegoś prostego wystarczy XHTML i CSS.
    Abstrahują od tematu Podobają mi się cienie używane w nagłówkach sekcji.

    • Przy tworzeniu aplikacji mobilnych naprawdę przydaje się coś takiego jak JQuery Mobile. Przede wszystkim wszystkie kontrolki, przyciski, itp. jest dostosowane do mniejszych, dotykowych wyświetlaczy. Robienie tego od podstaw w większości przypadków jest tylko stratą czasu.

      W nagłówkach sekcji używany jest styl CSS „text-shadow” :)

  4. czy w gwt css’y można robić „na zewnątrz” aplikacji – czyli dopinać je tak jak w stronach htmlowych?

    • Ogólnie aplikacja GWT jest zwykłą stroną HTML’ową, więc można zrobić wszystko tak jak w normalnym HTML’u. Każdy moduł GWT z EntryPoint’em posiada swoją stronę HTML, w której można załączyć wszystkie niezbędne pliki CSS.

Skomentuj


UWAGA - Możesz używać HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

CommentLuv badge