Making the Toast and Tea

Going Mobile with pyramid_jqm

Tres Seaver

tseaver@agendaless.com

Presenter Notes

About Me

  • Principal, Agendaless Consulting
python-logo.gif
  • Python web developer: 12 years
zope-logo.png
  • Zope developer: 11 years
  • CMF project lead: 11 years
  • Current president, Zope Foundation
repoze-logo.gif
  • Repoze project, 4 years

Presenter Notes

Overview

Moving from traditional web development to mobile development is hard.

Re-using what we as web developers know can ease the pain.

  • HTML, JS, and CSS are increasingly capable of delivering high-quality UX

pyramid_jqm capitalizes on that reuse

  • Provides a quick starting point for a useful, web-based mobile app.

Presenter Notes

Distributed development is hard

... especially compared to traditional web development

  • Everything is async
  • Tradeoffs: Consistency vs. performance

Presenter Notes

Distributed development is hard

Ugly new failure modes

oh_my.jpeg

Livelocks and deadlocks and netsplits, oh my!

Presenter Notes

Mobile development is harder

  • Slower, smaller machines deliver UX
  • Apps need to "degrade gracefully" for connectivity, real-estate.
whack-a-mole.jpeg
  • Explosion of platforms makes QA rough^Wimpossible

Presenter Notes

Native apps are no fun

  • New platform => new app
    • Conversion is not feasible (in general)
  • New (old) development paradigm
    • GUI vs. request-response

Presenter Notes

Native apps are no fun

sharecropping.jpg
  • Deep platform commitment ("sharecropping")

Presenter Notes

You already grok^Wknow webapps

Your bread-and-butter skills should be transferable:

  • Generating "customized" HTML is natural

  • Javscript can put "lively" UX in play

    ... even for curmudgeons like me

  • Caching and other tuning mechanisms are familiar ground.

Presenter Notes

HTML5 FT(mobile)W

Apps run using "stock" browser components.

dog_cat_with_umbrella.png

Testable on desktops (but watch out for performance!)

Mark Pilgrim's "Dive into HTML5" is the essential docs.

Presenter Notes

HTML5 Features: JSON

(OK, not really HTML5)

jason_fleece.jpg

But JSON rocks!

... and feels "Pythonic"

Presenter Notes

HTML5 Features: JSON

Return a Python dict or list directly.

Super testable.

Blazing speed compared to rendering HTML.

Presenter Notes

HTML5 Features: JSON

Factoring a web app to expose JSON can even benefit the traditional desktop UX:

  • "Base" HTML pages can be cached more efficiently.
  • "Dynamic" bits can be aded via JSON queries.

Presenter Notes

HTML5 Features: DOMStorage

The localStorage element stores key-value pairs as strings.

treasurechest.gif

Think "cookies on steriods", local to the "page" (the DOM).

Presenter Notes

HTML5 Features: DOMStorage

setItem, getItem, and removeItem are the API. Like Python, you can use square brackets.

You have to convert values yourself.

Lawnchair puts a slicker, more cross-platform face on it.

Presenter Notes

HTML5 Features: Geolocation

The geolocation element provides access to the API.

geo_location.png

Can use GPS, cell tower triangulation, IP mapping

Presenter Notes

HTML5 Features: Geolocation

API takes asynchronous callback function:

function show_map(position) {
    var latitude = position.coords.latitude;
    var longitude = position.coords.longitude;
    // Now show a map.....
}

navigator.geolocation.getCurrentPosition(show_map);

Presenter Notes

HTML5 Features: Offline manifest

Offline mode: the cache.manifest file gives fine-grained control over resources

unplug.jpg

Resources can be "never cache", "always cache", or "fallback"

Presenter Notes

HTML5 Features: Offline manifest

Each page in an app should reference the same cache.manifest URL:

<!DOCTYPE HTML>
<html manifest="/cache.manifest">

MIMEtype for that URL must be text/cache-manifest

Presenter Notes

HTML5 Features: Improved forms

HTML forms :(

angry_guy.png

and then add thumb-typing :|

Presenter Notes

HTML5 Features: Improved forms

Improved form elements are especially useful for mobile devices.

Declarative, client-side validation (required, plus custom types) can be an easy mobile win.

  • Unfortunately, devices / browsers support varies a lot, at least for now.

Presenter Notes

HTML5 Mobile Downsides

Performance: JS in HTML may be noticeably slower than native code

  • Especially with large data sets.

"Deep" access to platform APIs may be missing

  • But philikon is going to fix that :)

Integration with platform-specific stores missing.

Presenter Notes

Why jquery.mobile?

Supports all popular mobile device platforms and modern browsers:

  • iOS
  • Android
  • Blackberry
  • Palm WebOS
  • Nokia/Symbian
  • Windows Phone 7
  • MeeGo
  • Opera Mobile/Mini
  • Firefox Mobile
  • Kindle, Nook

Presenter Notes

Why jquery.mobile?

Built on the jQuery and jQuery UI foundation

Active, friendly community

Unfied touch / mouse event model.

AJAX page loads avoid "refresh" pain

Presenter Notes

Why jquery.mobile?

Progressive enhancement

Responsive design:

  • Both based on clean, semantic HTML5 markup

Flexible, easily themeable design.

Presenter Notes

Getting Started with pyramid_jqm

Create a new virtual environment:

$ /opt/Python-2.7.2/bin/virtualenv \
    --no-site-packages /path/to/jqmdemo

Install pyramid and pyramid_jqm from PyPI:

$ cd /path/to/jqmdemo
$ bin/easy_install pyramid pyramid_jqm

Presenter Notes

Getting Started with pyramid_jqm

Generate the app skeleton via paster create:

$ mkdir src && cd src
$ ../bin/paster create \
    -t pyramid_jqm_starter ploneconf

Install the generated package:

$ cd ploneconf
$ ../../bin/python setup.py develop

Presenter Notes

Getting Started with pyramid_jqm

Start the server:

$ ../../bin/paster serve --reload development.ini

Profit!

Presenter Notes

Walkthrough: Main Menu

demo-main_page.png

Presenter Notes

Walkthrough: Main Menu

Simple, semantic HTML:

<div data-role="page" id="home">

  <div data-role="header">
    <h1>pyramid_jqm</h1>
  </div>

  <div data-role="content">

    <ul data-role="listview" data-inset="true">
      <li data-role="list-divider">Demos</li>
      <li>
        <a href="#about">About</a>
      </li>

Hash URLs refer to other "pages" (divs) in the DOM.

Presenter Notes

Walkthrough: About Page

demo-about_page.png

Presenter Notes

Walkthrough: About Page

Static HTML provides markers:

<p>This application is using <i>pyramid_jqm</i> version
   <span id="about-pyramid-jqm-version"></span> and Pyramid version
   <span id="about-pyramid-version"></span>.</p>

... for update by jQuery:

function about_pageshow(div) {
    $.getJSON(api_prefix + '/versions.json', function (data) {
            $('#about-pyramid-jqm-version').text(data.pjqm_version);
            $('#about-pyramid-version').text(data.pyramid_version);
        }).error(jqxhr_error);
}

Presenter Notes

Walkthrough: Map Demo

demo-map_page.png

Presenter Notes

Walkthrough: Map Demo

Google Maps API initialized using "asynch" API (for bundling):

var script = document.createElement("script");
script.type = "text/javascript";
script.src = "http://maps.google.com/maps/api/js?sensor=false&callback=pyramid.init_google_maps";
document.body.appendChild(script);

Presenter Notes

Walkthrough: Map Demo

Using jquery.Deferred to manage async completions:

var google_maps_ready = new $.Deferred();
//...
function init_google_maps() {
    var map_options = {
         zoom: 15,
         mapTypeId: google.maps.MapTypeId.ROADMAP,
         mayTypeControl: false,
         //...
         zoomControl: true;
    };
    google_maps_ready.resolve(map_options);
}

Presenter Notes

Walkthrough: Map Demo

Geolocation API is queried, with completion through another deferred:

var device_location_ready = new $.Deferred();
//...
function set_device_location(loc) {
    device_location_ready.resolve(loc);
}
if (navigator.geolocation) {
    navigator.geolocation.getCurrentPosition(
      set_device_location, fail_device_location,
      {'timeout': 2000});
}

Presenter Notes

Walkthrough: Map Demo

When device location and maps API ready, initialize the map:

$.when(google_maps_ready, device_location_ready).done(
    function (map_options, loc) {
        var
            point = new google.maps.LatLng(loc.coords.latitude,
                                           loc.coords.longitude),
            local_map_options = {'center': point},
            canvas = $('#map-canvas')[0],
            map,
            marker;
        $.extend(local_map_options, map_options);
        map = new google.maps.Map(canvas, local_map_options);
        map_ready.resolve(map);
     });

Presenter Notes

Walkthrough: Form Page

demo-form_page.png

Presenter Notes

Walkthrough: Form Page

Simple, structural HTML:

<form id="personalinfo-form" method="post"
      action="/change_personalinfo">

 <fieldset data-role="fieldcontain">
 <label for="personalinfo-email">Email:</label>
 <input type="email" name= "email" id="personalinfo-email"
        class="required"/>

Presenter Notes

Walkthrough: Form Page

Simple Pyramid view code for drop-down population:

countrylist = [
    {'abbr': 'US', 'name': 'United States'},
    {'abbr': 'CA', 'name': 'Canada'},
    #...
]
@view_config(name='countries.json', renderer='json',
             http_cache=cache_one_day)
def countries(request):
  return countrylist

Presenter Notes

Walkthrough: Form Page

Populating the drop-down client side:

var countries_select = $('#personalinfo-country');
//...
countries_api(function (data) {
    countries_select[0].options.length = 0;
    $.each(data, function (index, item) {
        countries_select.append(
          $('<option value="' + item.abbr + '">' +
             item.name + '</option>'));
    });
    countries_select.selectmenu('refresh');
});

Presenter Notes

Walkthrough: Dynamic Pages Demo

demo-dynpages_list.png

Presenter Notes

Walkthrough: Dynamic Pages Demo

Static list has only the header:

<ul id ="dynpages-languages" data-role="listview" data-inset="true">
  <li data-role="list-divider">Languages</li>
</ul>

Add links (via JSON) to server-side URLs:

var ul = $('#dynpages-languages');
webframeworks_api(function (data) {
    ul.listview();
    $.each(data, function (index, lang) {
        var li = $('<li><a href="/language/' + lang.name + '">' +
                   lang.desc + '</a></li>');
        ul.append(li);
    });
    ul.listview('refresh');
});

Presenter Notes

Walkthrough: Dynamic Pages Demo

New pages get folded into the main DOM.

demo-dynpages_detail.png

Presenter Notes