layoutElementContent.uuid: 105511
layoutElementContent.uuid: 105652 Document Name 105652 Document UUID 105653

Flickr Demo Widget Tutorial

Contents

  1. Introduction
  2. Widget technology basics

    2.1 The widget runtime

    2.2 Widget structure

    2.3 Development tools

  3. Getting started

    3.1 The main document

    3.2 CSS definitions

    3.3 The widget configuration

    3.4 A first test

  4. Adding functionalities

  5. A closer look

    5.1 Initialize the widget

    5.2 Private variables and data stores

    5.3 Getting data using XHR

    5.4 Handling resolution change

    5.5 Store preferences within the widget

Introduction

In this tutorial we will create a little more advanced widget to show you how to perform asynchronous calls (AJAX), how to communicate with a RESTful API, how to handle JSON data, and how to store data in the widget's preference store. Furthermore, we walk you through the steps of packaging and deploying widgets for mobile phones.

Note

This demo widget needs a Flickr API key to work properly. If you do not have a Flickr API key, you may create one in your flickr.com account.

2. Widget technology basics

Essentially widgets are small web applications that are stored on the target device. Creating a widget is quite similar to creating a web page. You use the same technologies you would use to build a server-based web site. The difference lies in the runtime environment the widget runs in and in the format in which it is distributed.

2.1 The widget runtime

A Widget executes in a specialized web runtime context that does not feature the usual browser user interface. In this respect, the widget runtime resembles a site-specific browser.

If it's not already pre-installed on your mobile, you can download the Vodafone widget runtime - and SDK - here. At the time of this writing the runtime is available in the UK, Spain and Germany.

2.2 Widget structure

A Widget is distributed as a regular zip archive with .wgt file extension. This archive contains all the files necessary to run the widget. It must contain at least the following files:

  • The widget main document named index.html
  • The widget configuration file named config.xml

These files must either be located at the widget structure's root directory or in a sub-directory of the root directory. If placed in the root folder, additional files may be placed wherever you like. Usually script and css files are located in distinct sub-directories below the widget's root directory.

If the main document and configuration files are placed in a sub-directory, then any subsequent files must be placed in a directory that resides within this sub-directory.

2.3 Development tools

In addition to your standard set of web development tools you will need a zip file manager like 7-Zip to package your widget. You may also want to try Opera's widget emulator during development to simulate the device's widget runtime environment on your development system. The widget can be opened in the standard Opera browser.

To make creation of widgets very easy, there is the Widget Packager, which will help you creating all necessary files and will also help you including common JavaScript frameworks. It will also assist you on building and deploying your widget onto mobile devices for testing purpose. You may download the Widget Packager here.

3. Getting started

Within this tutorial we will create five files: the two mandatory files config.xml and index.html, a file named style.css that contains all CSS definitions, a file named Flickr.js that contains all of our JavaScript code, and a file named Tools.js that contains an MD5 hash generator and a JSON parser.

3.1 The main document

The main document is automatically loaded when your widget is opened in the Opera browser or the widget runtime. This file must be named index.html. Start developing your widget by creating a new directory - the root directory -, copy the code below into your favorite editor and save it to a file named index.html in the newly created directory.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
                      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <!-- Opera widget emulator -->
    <!-- <script type="text/javascript">
             if(parent.emulator)parent.emulator.begin(window);</script> -->

    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title>Flickr v0.0.1</title>

    <!-- Include CSS information -->
    <link rel="stylesheet" type="text/css" href="css/style.css" />
  </head>
  <body>
    <!-- Wrapper [start] -->
    <div id="wrapper">

      <!-- Header [start] -->
      <div id="header">
        <img src="css/images/flickr_logo.png" alt="Flickr" />
      </div>
      <!-- Header [end] -->

      <!-- Main content area [start] -->
      <div id="main" class="clear">

        <!-- View: Start screen [start] -->
        <div id="view_startScreen">
          <h1>Welcome</h1>
          <p>This is a demo widget to demonstrate a couple of things:</p>
          <ul>
            <li>Perform asynchronous calls</li>
            <li>Retrieve JSON-data</li>
            <li>Communicate with an RESTful API</li>
            <li>Store data in the widget's preference store</li>
          </ul>
          <h2>Demonstrations</h2>
          <p>Please choose from the following demonstrations:</p>
          <ul>
            <li id="demo1"><a href="#">Authorize the widget</a></li>
            <li id="demo2"><a href="#">View public photo stream</a></li>
            <li id="demo3"><a href="#">Search</a></li>
          </ul>
        </div>
        <!-- View: Start screen [end] -->

        <!-- View: Authorize widget [start] -->
        <div id="view_authorizeWidget" class="view hide">
          <h1>Flickr Authentication</h1>
          <p>To allow this widget to access your data on flickr.com, please click the button
             below and follow the instructions
             (you may have to login into your yahoo account first).</p>
          <p>Return to this window after you have finished the authorization
             process on flickr.com.<br />
             Once you are done, click the 'Complete Authorization' button.</p>
          <p><button id="btn_startWidgetAuthorization" disabled="disabled">
              Generating URL... Please wait...</button></p>
          <p><button id="btn_completeWidgetAuthorization"
              class="hide">Complete Authorization</button></p>
          <p id="view_authroizeWidget_status"></p>
        </div>
        <!-- View: Authorize widget [end] -->

        <!-- View: Public Photo Stream [start] -->
        <div id="view_publicPhotoStream" class="view hide">
          <h1 class="float_left">Public Photo Stream</h1>
          <img id="btn_closePublicPhotos"
               class="button btn_close float_right" alt="Close" src="" />
          <img id="btn_reloadPublicPhotos"
               class="button btn_reload float_right" alt="Reload" src="" />
          <br class="clear" />
          <div class="float_left">
            <a id="view_publicPhotoStream_nav_previous" class="hide" href="#">
                <img src="css/icons/arrow_left.png" alt="Previous" /></a>
          </div>
          <div class="float_right">
            <a id="view_publicPhotoStream_nav_next" class="hide" href="#">
                <img src="css/icons/arrow_right.png" alt="Next" /></a>
          </div>
          <div id="view_publicPhotoStream_photos"></div>
        </div>
        <!-- View: Public Photo Stream [end] -->

        <!-- View: Search [start] -->
        <div id="view_search" class="view hide">
          <h1 class="float_left">Search</h1>
          <img id="btn_closeSearch" class="button btn_close float_right" alt="Close" src="" />
          <p class="clear">
            <form>
              <input type="text" id="search_terms" name="search_terms" /><br />
              <button id="btn_search">Search</button>
            </form>
          </p>
          <p id="view_search_status" class="clear"></p>
          <div class="float_left">
            <a id="view_search_nav_previous" class="hide" href="#">
               <img src="css/icons/arrow_left.png" alt="Previous" /></a>
          </div>
          <div class="float_right">
            <a id="view_search_nav_next" class="hide" href="#">
               <img src="css/icons/arrow_right.png" alt="Next" /></a>
          </div>
          <div id="view_search_photos"></div>
        </div>
        <!-- View: Search [end] -->

        <!-- View: Single Photo [start] -->
        <div id="view_singlePhoto" class="view hide">
          <h1 class="float_left">Single Photo</h1>
          <img id="btn_closeSinglePhoto" class="button btn_close float_right" alt="Close" src="" />
          <div id="view_singlePhoto_photo" class="clear"></div>
        </div>
        <!-- View: Single Photo [end] -->

      </div>
      <!-- Main content area [end] -->
    </div>
  </body>
</html>

3.2 CSS definitions

Additionally, create a new directory named css within the root directory. Then, copy the code below into your favorite editor and save it to a file named style.css and store that file into the css directory.

@media all {
  body {
    background-color: #fff;
    color: #000;
    font-family: Arial, Helvetica, Verdana, sans-serif;
    font-size: 100.01%;     /* 16px */
    margin: 0;
    padding: 0;
  }

  a, a:link, a:visited, a:hover, a:active {
    color: #0063dc;
    text-decoration: none;
  }

  form {
    margin: 8px;
  }

  h1 {
    font-size: 100%;
    margin: 4px 0 0 4px;
  }
  h2 {
    font-size: 90%;
    margin: 8px 0 2px 4px;
  }

  input {
    padding: 2px;
    margin-bottom: 4px;
  }

  ul {
    font-size: 90%;
    list-style-type: square;
    margin: 0;
    padding: 0 0 0 24px;
  }

  p {
    font-size: 90%;
    line-height: 125%;
    margin: 4px 0 4px 4px;
  }


  /*  Header [start] */
  #header {
    font-size: 63%;
    height: 20px;
  }
  #header span {
    margin: 4px 4px 0 0;
  }
  #header img {
    margin: 2px 0 0 4px;
  }
  /*  Header [start] */


  /* Main content wrapper [start] */
  #main {
    font-size: 75%;
  }
  /* Main content wrapper [end] */


  /* Navigational elements [start] */
  #view_publicPhotoStream_nav_previous,
  #view_publicPhotoStream_nav_next,
  #view_search_nav_previous,
  #view_search_nav_next {
    background-color: #fff;
    height: 16px;
    padding: 4px;
    width: 16px;
  }
  /* Navigational elements [end] */


  /* View: Authorize widget [start] */
  #view_authorizeWidget {
    top: 0;
    left: 0;
  }
  /* View: Authorize widget [end] */


  /* View: Public photo stream [start] */
  #view_publicPhotoStream {
    top: 20px;
  }
  #view_publicPhotoStream_photos {
    padding-top: 8px;
    margin: 0 24px;
    min-height: 240px;
  }
  /* View: Public photo stream [end] */


  /* View: Single photo [start] */
  #view_singlePhoto {
    top: 20px;
  }
  #view_singlePhoto_photo {
    padding-top: 8px;
  }
  /* View: Single photo [end] */


  /* View: Search [start] */
  #view_search {
    top: 20px;
  }
  #view_search input {
    padding: 4px;
    border: 1px solid #999;
    width: 200px;
  }
  #view_search_photos {
    padding-top: 8px;
    margin: 0 24px;
    min-height: 240px;
  }
  /* View: Search [start] */


  /* Generic classes [start] */
  .clear {
    clear: both;
  }
  .clearfix:after {
    content: ".";
    display: block;
    font-size: 0;
    height: 0;
    clear: both;
    visibility: hidden;
  }
  .hide {
    display: none;
  }
  .float_left {
    float: left;
  }
  .float_right {
    float: right;
  }


  .btn_reload {
    margin: 2px;
    width: 10px;
    height: 10px;
    background: transparent url(icons/reloadButton10px.png) no-repeat 0 0;
  }
  .btn_close {
    background: transparent url(icons/closeButton10px.png) no-repeat 0 0;
    height: 10px;
    margin: 2px;
    width: 10px;
  }
  .button:hover {
    background-position: 0 50%;
  }
  .button:active {
    background-position: 0 100%;
  }

  .loadIndicator {
    float:right;
    margin: 4px;
  }

  .view {
    background-color: #fff;
    min-height: 300px;
    position: absolute;
    width: 100%;
  }
  .photo {
    padding: 2px;
    background-color: #eee;
    border: 1px solid #999;
    margin-right: 4px;
  }
  /* Generic classes [end] */
}

Within this widget, we are going to use a couple of images. Please download this zip archive and extract the contents of the archive into the css directory, keeping folder information of the files in the archive intact. After that, your folder structure should look like this:

folder structure

 

Go ahead and open the index.html file in a browser (e.g. Firefox, Opera etc.)

Index in browser

 

3.3 The widget configuration

A basic widget configuration needs at least an identifier, a name and the size of the widget. Because we want to communicate with the Flickr API, we also need to specify the hosts (in this case it is only one) our widget will call during the widget's execution. Copy the following code into a new file named config.xml and save it into the widget's root directory you created earlier.

<widget id="http://lab.vodafone.com/flickr">
  <widgetname>Flickr</widgetname>
  <description>This is a flickr demo widget</description>
  <width>240</width>
  <height>320</height>
  <security>
    <access>
      <host>m.flickr.com</host>
    </access>
  </security>
</widget>

3.4 A first test

There are two possibilities to package the widget's files for deployment: * Create an archive of all files * Create an archive of the directory containing all the widget files

You'll need to rename the archive's .zip extension to .wgt for it to be recognized as a widget by the widget runtime or the Opera browser. That .wgt archive can then be double clicked, or opened in the Opera browser to start the widget.

Widget in Opera

 

If you've got the runtime installed on your mobile device, you can upload the widget to your device and test it there. There are several ways you can transfer your widget:

  • Transfer via Bluetooth
  • Copy to the mobile's SD-card
  • Deploy to a web server and download

The widget manager on the mobile device will recognize the file type and will provide you with the option to install the widget.

4. Adding functionalities

You probably noticed that the widget does not do much but showing a text and a disabled button. To change this, create a new folder named js within the widget's root directory, download flickr.js and [tools.js](/bvcms/applications/widgets/tools.js">Tools.js, and save them into the newly created directory.

Now we need to load these two JavaScript files in our index.html file. So open index.html and add the following code to the :

<!-- Include JavaScript files -->
<script type="text/javascript" src="js/Tools.js" charset="utf-8"></script>
<script type="text/javascript" src="js/Flickr.js" charset="utf-8"></script>

<!-- Initialize the widget -->
<script type="text/javascript">
  Flickr.init();
</script>

To actually communicate with the Flickr API, you need to insert your API key information. Open Flickr.js and find the following code (should start on line 27):

var _auth = {
  apiKey : '__YOUR_API_KEY__',
  frob: null,
  secret : '__YOUR_API_KEY_SECRET__',
  token: (widget.preferenceForKey('_auth.token') != null ? 
          widget.preferenceForKey('_auth.token') : null)
};

Replace YOURAPIKEY with your actual Flickr API key and replace YOURAPIKEY_SECRET with your actual Flickr API key secret and save the file

After that, our widget is ready to be packaged and deployed (s. A first test ). Go toy around with it, then come back here to read more about what we do within the widget. You may also take a look into Flickr.js to get an deeper insight into what we are doing within the widget. There you will find comments on all the methods and variables.

5. A closer look

5.1 Initialize the widget

To make sure that we access the DOM after it has been loaded completely, we call Flickr.init() from within our index.html. This method than adds an event listener that listens for the DOMContentLoaded event. This event is fired after the DOM was loaded completely. Everything our widget does is then defined within the event handler.

Instead of adding an event listener for the DOMContentLoaded event, you may add an event handler for the onload event and handle widget initialization there.

document.addEventListener('DOMContentLoaded', function () {
  [...]
}, false);</pre>

5.2 Private variables and data stores

Let us take a look at some private variables:

First, we store the URL to flickr so that we do not need to enter it over and over again. Make sure that all hosts you want to access within your widget are in the security section of your config.xml.

var _host = 'http://m.flickr.com/';
var _url = {
  api: _host + 'services/rest/?',
  auth: _host + 'services/auth/?',
  feed: _host + 'services/feeds/'
};

Second, to keep the server calls to a minimum, we store information inside the widget. Therefore we create an object that functions as data store and we store all data/information we do not want to load from the server every time into this object.

var _dataStore = {
  publicPhotos: { },
  search: { },
  user: { }
};

5.3 Getting data using XHR

To receive data from a server you can use XHRs just as you would do in any other web application. Just create the XHR object and handle the response from the server. In the code below, we did that for retrieving public photos from Flickr. We explicitly tell the API to return the data in json format. If the request was successful, we parse the data with our JSON parser (s. Tools.js) and then store the parsed data in our _dataStore object to make it available throughout the widget. Finally, we fire a custom event to signal, that the photos were loaded successfully.

var _getPublicPhotos = function() {
  // Create URL that points to the public photos flickr stream
  var url = _url.feed + 'photos_public.gne?<em>format=json</em>';

  // Create and send XHR
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (this.readyState == 4) {
      if (this.status == 200) {
        // Get rid of jsonFlickrFeed() submited by Flickr
        var responseText = this.responseText.replace(/^jsonFlickrFeed(/, '').replace(/)$/, '');

        // Store json of the public photos in our data store
        <em>_dataStore.publicPhotos = JSON.parse(responseText);</em>

        // Fire an event to signal, that a new set of public photos has been loaded
        <em>_fireEvent('onPublicPhotosLoaded', document);</em>
      }
    }
  };
  xhr.open('GET', url, true);
  xhr.send();
};

5.4. Handling resolution change

When the user rotates his/her mobile device (e.g. from portrait to landscape), the widget object fires an event to signal that the resolution has changed. You should add an event listener and handle this resolution change. What we did in our Flickr widget is to resize the widget to match the new screen resolution and therefore maximize the widget.

var _maxWidgetSize = function() {
  // Get the actual screen width and screen height
  var screenHeight = window.screen.height;
  var screenWidth = window.screen.width;

  // Resize the widget
  window.resizeTo(screenWidth, screenHeight);
};

[ ... ]

widget.addEventListener('resolution', function() {
  // Resize the widget to the maximum screen width and height
  _maxWidgetSize();
}, false);

5.5 Store preferences within the widget

If you want to store preferences for your widget, you can do so by calling the setPreferenceForKey() method of the widget object. To get data stored within the widget you may use the preferenceForKey() method of the widget object. These preferences are persistent and therefore available after you close the widget and then reopen it again, for example.

In our Flickr widget, we use this mechanism to store the authentication token from Flickr that authorizes our widget to access the Flickr data of the user. When the user then reloads the widget, we do not have to authorize the widget again, but instead load the authentication token from the widget's preference store. You already saw that bit of code when your added your Flickr API key information.

[...]
_auth.token = this.responseXML.getElementsByTagName('token')[0].firstChild.nodeValue;
widget.setPreferenceForKey(_auth.token, '_auth.token');
[...]
var _auth = {
  apiKey : '__YOUR_API_KEY__',
  frob: null,
  secret : '__YOUR_API_KEY_SECRET__',
  token: (widget.preferenceForKey('_auth.token') != null ? 
          widget.preferenceForKey('_auth.token') : null)
};