Going Native with PhoneGap

With so many mobile devices in the market, more and more apps being developed. It kind of reminded me of the early day of the web when new browser came up one by one. In the mobile world, we have to deal with different devices and choose between web or native app. With HTML 5, our web app can do some of the things that a native app can. If you are familar with Sench Touch, the framework provides features such as touch event, geo location, offline storage and rich UI library. So, what are the things that may driver us to go native? at the top of the list is accessing the devices features & hardware (e.g. accelerometer, photo, contacts, vibration, etc). The 2nd driver could be $, meaning selling our apps in the App Store.

PhoneGap - Closing the Gap 🙂

Let me introduce PhoneGap to bridge the gap. As you can see from the diagram, PhoneGap is an open source framework that act as a bridge between web apps and mobile devices. Brilliant, har!! The framework allows you to:

  • package your web app code and resources into a binary app and deploy to multiple platforms (currently it supports 6 platforms; iOS, Android, Windows Phone 7 (coming soon), Blackberry, Palm/HP webOS and Symbian.
  • access native features such as accelerometer, camera, compass, contacts, file, geo location, storage, meaid and notification. The features x devices/OS matrix is available here.

Interestingly, You can also compile your code in the cloud via build.phonegap.com. It is still in beta, so it is free. The pricing details will be announced near its launch date.

My thought:

  • For now, building graphic intensive apps may not be suitable. Coding in the device native language may be better. But with the advancement in HTML 5, things may change.
  • The first stable release (v0.6.0) of PhoneGap was in Feb 2009 and now it is at version 0.9.4. Will there be big changes before it reaches 1.0?
  • Using PhoneGap means you must keep up with the devices OS version & PhoneGap. Hopefully the release can be fast enough since it is supporting so many devices.
  • For iOS, the framework integrate with XCode and it is very easy to jump start.
  • The documentation is good. It is organised base on the features and each has example.

Useful links:

I downloaded and started to play with the framework a little. I migrated my USGS app – which is based on Sencha Touch – to a native iPhone app. I have yet to explore its native APIs in depth. Just wanted to share the framework.

Happy hacking!!!

 

my USGS app in iPhone Simulator

Sencha Touch MVC Application – Part 2

In my previous post, I shared presentation on Sencha Touch MVC. That’s all the “talk”, it is time to see it in action. This short tutorial will walk through the implementation of Sencha Touch MVC application. I revamped my old (and long forgotten & messy) USGS apps as example. Before I dive into the code, I will share how to structure the apps.

Structuring Your Apps

Firstly, create the folder structure to support the MVC application. The figure below shows the basic structure required and the codes are available here. I personally think this is a good practice and can be use as coding standard and guideline for Sencha Touch apps. In addition to that, you may want to standardize the file & object naming convention for the controllers, views and data models.

  • m.usgs – the root folder for the application
    • app – folder for your sencha touch code, containing app.js and folders for controllers, models and views.
    • lib – folder for sencha library and other plug-in. (in my example, I moved the sencha library out for sharing with other apps)
    • resources – folder for web resources such as css & images.
    • index.html – the main page to launch the apps.
Sencha Touch - Structuring Your Apps for MVC
Sencha Touch - Structuring Your Apps for MVC

Coding

Step 1: Instantiating the Application

Create app/app.js and add the basic launcher for an Ext.Application instance. In the example it will be namespaced as “usgs”.

Ext.regApplication({
    name: "usgs",
    launch: function() {
	this.views.viewport = new this.views.Viewport();
    }
});

Create a main Viewport view which will house the pages required for displaying USGS Earthquake data. Here we place references to the card instances in the “usgs.views” namespace, so that we can refer to them explicitly in our page flow.

usgs.views.Viewport = Ext.extend(Ext.Panel, {
    fullscreen: true,
    layout: 'card',
    cardSwitchAnimation: 'slide',
    initComponent: function() {
        //put instances of cards into app.views namespace
        Ext.apply(usgs.views, {
        	usgsMenu: new usgs.views.UsgsMenu()
        	,usgsList: new usgs.views.UsgsList()
        	,usgsMap: new usgs.views.UsgsMap()
        });
        //put instances of cards into viewport
        Ext.apply(this, {
            items: [
                usgs.views.usgsMenu
                ,usgs.views.usgsList
                ,usgs.views.usgsMap
            ]
        });
        usgs.views.Viewport.superclass.initComponent.apply(this, arguments);
    },
    layoutOrientation : function(orientation, w, h) {
        usgs.views.Viewport.superclass.layoutOrientation.call(this, orientation, w, h);
    }
});

Step 2: Modelling Data

Like any MVC-based application the next step is to prepare for the data model. In the models folder, create a definition of the USGS model. The data structure match the content structure is USGS website. In order to demonstrate the use of JSON, I used YQL to convert the RSS data from USGS.

Ext.regModel("usgs.models.UsgsData", {
    fields: [
      {name: 'id', 		type: 'int'},
      {name: 'title', 		type: 'string'},
      {name: 'description', 	type: 'string'},
      {name: 'link', 		type: 'string'},
      {name: 'pubDate', 	type: 'date'},
      {name: 'lat', 		type: 'string'},
      {name: 'long', 		type: 'string'}
    ]
});

Create a simple data store to retrieve the data. For cross domain ajax, set the proxy type to ‘scripttag’. It also handles the callback. 🙂

usgs.stores.usgsData = new Ext.data.Store({
    model: 'usgs.models.UsgsData',
    proxy: {
    	type: 'scripttag',
    	url: 'http://query.yahooapis.com/v1/public/yql',
    	extraParams: {
    		format: 'json'
    	},
	  	reader: {
	  		root: 'query.results.item'
	  	}
  	}
});

Step 3: Creating the Views

The data is ready, now we start to create the view for the USGS data. It allows us to test the data store as well. In this example, I have 3 views:

  1. USGS Menu – this view contains list of data we can retrieve from USGS
  2. USGS List – based on the selected menu, this view shows the list of USGS earthquake data.
  3. USGS Map – this view shows the map where the earthquake occurred.

Here I will just show one of the view. Ignore the call to the controller (Ext.dispatch(…)) for now, wire the view when the controller is ready.

usgs.views.UsgsList = Ext.extend(Ext.Panel, {
    dockedItems: [{
        xtype: 'toolbar',
        title: 'USGS',
        dock: 'top',
        items: [{
        	xtype: 'button',
          text: 'Back',
          ui: 'back',
          handler: function() {
            Ext.dispatch({
                controller: usgs.controllers.usgsController,
                action: 'backToIndex'
            });
          },
          scope: this
        }]
    }],
    items: [{
        xtype: 'list',
        emptyText   : 'No data available.',
        store: usgs.stores.usgsData,
        itemTpl: '{title}',
        onItemDisclosure: function (record) {
            Ext.dispatch({
                controller: usgs.controllers.usgsController,
                action: 'showMap',
                data: record.data
            });
        },
        grouped: false,
        scroll: 'vertical',
        fullscreen: true
    }],
    initComponent: function() {
        usgs.views.UsgsList.superclass.initComponent.apply(this, arguments);
    }
});

Step 4: Building the Controller

We reached the last step and this is the point where all the action and view wired together. If you look back at the view, the handler dispatches specific action to the controller. The actions are defined in the methods below and the controller will then manage the page flow by activating the card. Switching page is done by calling the setActiveItem() and it is applicable because my Viewport is using card layout.

usgs.controllers.usgsController = new Ext.Controller({

    index: function(options) {
        usgs.views.viewport.setActiveItem(
            usgs.views.usgsMenu, options.animation
        );
    },

    showMap: function(options) {
        var data = options.data;
    	usgs.views.usgsMap.addMap(data);
        usgs.views.viewport.setActiveItem(
           usgs.views.usgsMap, options.animation
        );
    },

    showUsgsList: function(options) {
        var id = parseInt(options.id);
    	usgs.stores.usgsData.getProxy().extraParams.q = usgs.query[id].q;
    	usgs.stores.usgsData.read();
        usgs.views.viewport.setActiveItem(
            usgs.views.usgsList, options.animation
        );
    },

    backToIndex: function(options) {
        usgs.views.viewport.setActiveItem(
            usgs.views.usgsMenu, options.animation
        );
    },

    backToUsgsList: function(options) {
        usgs.views.viewport.setActiveItem(
            usgs.views.usgsList, options.animation
        );
    }
});

If you are not using card layout then the controller will need to render the UI component. In the sample code below the view is rendered using the Ext.Controller render() and listener is added to catch the event bubble from the view.

list: function() {
    this.listView = this.render({
        xtype: 'myMainPanel',
        listeners: {
            scope : this,
            filter: this.onFilter,
            selectionchange: this.onSelected
        }
    }, Ext.getBody()).down('.dataList');
}

The screenshots below are the result from the above tutorial.

Implementing the structure and MVC into Sencha Touch definitely helps to simplify my design. I can easily implement the page flow and not end up with spaghetti code. It also helps to separate the view, data and controller and make the code more maintainable.

Hope it is useful and happy hacking!

Sencha Touch MVC Application – Part 1

Developing MVC application is nothing new and there are many frameworks to help us implement MVC, e.g. Struts & Spring MVC (JEE), ASP.NET MVC, JavaScriptMVC, etc. If you are a mobile apps developer and using Sencha Touch, you will most likely wonder whether or not you can apply MVC to simplify your apps design and make it more maintainable.

Here I am sharing a useful presentation by Tommy Maintz (@tommymaintz). This presentation will bring you a step further into a better Sencha Touch mobile apps design by structuring your apps and applying MVC pattern.

Dynamically Loading Ext.NestedList

After developing the USGS earthquake for mobile, I want to extend it to display more information. One of the challenge I faced was how to dynamically load data into the nested list. I did a bit reading in the forum and someone suggested to implement the following.

var nestedList = new Ext.NestedList({
items: []
});

var list = [
{
text: 'Test'
},{
text: 'Test 2'
}];

nestedList.setList(list, true);

I did try out to use setList(), however I noticed a problem when I clicked the back button. The nested list still remember the original “items” which was set to blank or []. It is correct that setList() will rebuild the entire list, however in this instance it is not the right method to use. One thing to highlight is the setList() is called when item is tapped and when we click back button, however everytime it is called the new list is appended to the end of the collection as shown in the code below. As we tap the item and click back, the index counter become wrong.

if (!this.lists.contains(list)) {
this.lists.push(this.add(list));
}

I will cover 2 scenarios related to this topic:

  1. Dynamically load nested list after the UI is launched.
  2. Dynamically load list when item is tapped.

The alternative solution to dynamically set the nested list is to extend the original object and implement a new method “reset” as shown below. Perform exactly as It is in setList() but instead of joining the list, reset it. This will address scenario 1.

Ext.ws.NestedList = Ext.extend(Ext.NestedList, {
resetList : function(list, init) {
//...
if (!this.lists.contains(list)) {
this.lists[0] = this.add(list);
}
//...
},

onItemTap : function(item) {
//...
}
});

In order to address scenario 2, override the onItemTap() as shown below. In this example, I added a new attribute “fid” into the item which helps to decide what data to load. In your implementation you might call JSONP request to load the data.

Ext.ws.NestedList = Ext.extend(Ext.NestedList, {
resetList : function(list, init) {
//...
},

onItemTap : function(item) {
item.el.radioClass('x-item-selected');
if (item.items) {
this.backButton.show();
if (item.fid == 'A') item.items = [ {text: 'List Z.1'} ];
this.setList(item);
this.listIndex++;
}
this.fireEvent('listchange', this, item);
}
});

The following is the full source code used to demonstrate the 2 scenarios. If you have alternative solution to the above, do drop me comment and share it. 🙂

<script type="text/javascript">

Ext.ns('Ext.ws');

Ext.ws.NestedList = Ext.extend(Ext.NestedList, {
resetList : function(list, init) {
var items = init ? list : list.items;
if (!list.isList) {
list = new Ext.Container({
isList: true,
baseCls: 'x-list',
cls: 'x-list-flat',
defaults: {
xtype: 'button',
baseCls: 'x-list-item',
pressedCls: 'x-item-pressed',
ui: null,
pressedDelay: true
},
listeners: {
afterrender: function() {
this.getContentTarget().addClass('x-list-parent');
}
},
scroll: 'vertical',
items: items,
text: list.text
});
}

this.lists = this.lists || [];
if (!this.lists.contains(list)) {
this.lists[0] = this.add(list);
}

var isBack = (this.lists.indexOf(list) < this.lists.indexOf(this.activeItem));
if (this.rendered) {
this.setCard(list, init ? false : {
type: this.animation,
reverse: isBack
});
}
this.activeItem = list;
},

onItemTap : function(item) {
item.el.radioClass('x-item-selected');
if (item.items) {
this.backButton.show();
if (item.fid == 'A') item.items = [ {text: 'List Z.1'} ];
this.setList(item);
this.listIndex++;
}
this.fireEvent('listchange', this, item);
}
});

Ext.setup({
onReady: function() {

var list = [ {text: 'List A', fid: 'A', items: [] },
{text: 'List B', fid: 'B', items: [ {text: 'List B.1'} ] },
{text: 'List C', fid: 'C', items: [ {text: 'List C.1'} ] }
];

var nestedList = new Ext.ws.NestedList({
items: [],
fullscreen: true
});

nestedList.resetList(list, true);

} // end onReady
}); // end ext.setup

</script>

Update: the example above was based on Sencha Touch 0.91

USGS Earthquakes Map for Mobile

Another good example of how fast and easy it is to develop a mobile application using Sencha Touch. I spent 2 nights and finally get it up and running. It is a very simple application that read feed from USGS feed and plot the earthquake location into Google Maps. For this application, I used the “kitchen sink” example as the base. It is a really good example to start with. The UI implementation allows the apps to run both on desktop (on HTML5 ready browser) and mobile without compromising the look and feel. See the screenshot below. The mobile version has the iPhone UI touch 🙂

I did encounter one problem during the implementation. The USGS data is in XML format, so I tried using the XMLReader provided in Ext. Somehow, I keep getting error from the XMLReader saying I am accessing property (totalProperty) on undefined object. Not sure whether or not it is a bug. Thanks to YQL, I converted the XML feed to JSON. The SQL like syntax looks friendly, see below

var makeAjaxRequest = function() {
var params = {
rssUrl: 'http://earthquake.usgs.gov/earthquakes/catalogs/shakerss.xml'
};
var usgsRssQuery = new Ext.Template('select * from rss where url="{rssUrl}"');
var query = usgsRssQuery.applyTemplate(params);

Ext.util.JSONP.request({
url: 'http://query.yahooapis.com/v1/public/yql',
callbackKey: 'callback',
params: {q: 'select * from rss where url="<a href="http://earthquake.usgs.gov/earthquakes/catalogs/shakerss.xml&quot;'">http://earthquake.usgs.gov/earthquakes/catalogs/shakerss.xml"'</a>, format: 'json'},
callback: function(data) {
// Process feed data.
}
});
}

The apps UI comprise of 2 part, first is the list that display the feed data and second is the panel that display the map. The main UI is based on the nested lists components and using onListChange event to handle touch or click on the data item. Here is the setup

this.navigationPanel = new Ext.NestedList({
items: this.navigationItems || [],
dock: 'left',
width: 250,
height: 456,
hidden: !Ext.platform.isPhone && Ext.orientation == 'portrait',
toolbar: Ext.platform.isPhone ? this.navigationBar : null,
listeners: {
listchange: this.onListChange
,scope: this
}
});
var pos=new google.maps.LatLng(feedArray[i].lat, feedArray[i].long);
var desc = feedArray[i].description;
var map = new Ext.Map({
title: i,
markerDesc: desc,
markerPos: pos,
mapOptions: {
zoom: 5,
mapTypeId: google.maps.MapTypeId.ROADMAP
,center: pos
},
listeners: {
delay: 500,
afterrender: function(){
addMarker(this.map, this.markerPos, this.markerDesc);
}
}
});
var panel = new Ext.Panel({
items: [map]
});

Similar to the Nearby Tweet, I couldn’t test it on Android. If you do try it on android, drop a comment and share whether or not it works. To try the apps visit http://bit.ly/a7KJj4

Note: For desktop, try on HTML5 ready browser. I used Chrome to test it out.

Update: See the updated version implemented using MVC here.

Nearby Tweet (Makeover)

After spending my weekend reading Sencha Touch, I decided to do a bit of makeover for my “Nearby Tweet”. Sencha Touch kind of save me the time from learning the native mobile SDK like iOS or Andoid. It is using standards such as JavaScript, HTML 5 and CSS3, all I need to learn is the framework.

The previous version has a very important limitation which no geolocation, it is hardcoded to Singapore. Using the Geolocation feature in HTML 5, the application is now able to locate the user and mark the nearby tweets into the map. Another improvement is the use of “Tab Panel” to show both map and the tweets. Here is the screenshots:

So , how is it done?

Step 1: Include Sencha Touch CSS & JavaScript, Google Map API and own CSS & JavaScript.

<!DOCTYPE html>
<html>
<head>
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
 <title>Nearby Tweets Mobile</title>
 <link rel="stylesheet" href="css/ext-touch.css" type="text/css">
 <link rel="stylesheet" href="css/NearByTweetsMobile.css" type="text/css">
 <script type="text/javascript" src="<a href="http://maps.google.com/maps/api/js?sensor=true">http://maps.google.com/maps/api/js?sensor=true</a>"> </script>
 <script type="text/javascript" src="js/ext-touch.js"> </script>
 <script type="text/javascript" src="js/NearByTweetsMobile.js"> </script>
</head>
<body> </body>
</html>

Step 2: Implement the JavaScript for loading the UI, getting the Tweets and marking it on the map. There no change to the code that add marker and get location from previous implmentation.

Ext.setup({
    icon: 'icon.png',
    glossOnIcon: false,
    onReady: function() {
      // nbTweets - tweet list. Using Template to generate the html.
        var nbTweets= new Ext.Component({
            title: 'Nearby Tweets',
            scroll: 'vertical',
            tpl: [
                '<tpl for=".">',
                    '<div>',
                            '<div><img src="{profile_image_url}" /></div>',
                            '<div>',
                                '<h2>{from_user}</h2>',
                                '<p>{text}</p>',
                                '<p>{location}</p>',
                            '</div>',
                    '</div>',
                '</tpl>'
            ]
        });

        var map = new Ext.Map({
            title: 'Map',
            getLocation: true,
            mapOptions: {
                zoom: 12,
                mapTypeId: google.maps.MapTypeId.ROADMAP
            }
        });
       
    var geocoder = new google.maps.Geocoder();

    // Create tab panel for the map and tweet list timeline
        var panel = new Ext.TabPanel({
            fullscreen: true,
            animation: 'slide',
        ui: 'light',
            items: [map, nbTweets]
        });

    // handler for refresh button    
        var refresh = function() {
            var coords = map.geo.coords; // get user geolocation

            Ext.util.JSONP.request({
                url: 'http://search.twitter.com/search.json',
                callbackKey: 'callback',
                params: {
                    geocode: coords.latitude + ',' + coords.longitude + ',' + '10km',
                    rpp: 30
                },
                callback: function(data) {
                    data = data.results;

                    // Update the tweets in nbTweets
                    nbTweets.update(data);
                    // Add points to the map
                    for (var i = 0, ln = data.length; i < ln; i++) {
                        var tweet = data[i];
                        getTweetLocation(map, geocoder, tweet);
                    }
                }
            });
        };

        map.geo.on('update', refresh);

        var tabBar = panel.getTabBar();
        tabBar.addDocked({
            xtype: 'button',
            ui: 'mask',
            iconCls: 'refresh',
            dock: 'right',
            stretch: false,
            align: 'center',
            handler: refresh
        });

    }
});

// These are all Google Maps APIs
function getTweetLocation(map, geocoder, tweet) {
 // insert the code to get tweet location either from the geotag or tweet.location
    if (tweet.geo && tweet.geo.coordinates) {
        var position = new google.maps.LatLng(tweet.geo.coordinates[0], tweet.geo.coordinates[1]);
    addMarker(map, position, tweet);
    } else {
    var geocode = tweet.location.split(": ",2);
    var addr = geocode[1];
    if (addr == undefined) addr = tweet.location;
      if (geocoder) {
        geocoder.geocode( { 'address': addr }, function(results, status) {
          if (status == google.maps.GeocoderStatus.OK) {
           addMarker(map, results[0].geometry.location, tweet);
          } else {
            //alert("Geocode was not successful for the following reason: " + status);
          }
        });
      }
    } // end if tweet.geo
} // end addMarker
       
function addMarker(map, position, tweet) {
 // insert the code to add marker to map.
} // end addMarker

So far, I have tested in iPhone Safari (on iOS4) and it works fine. Not sure how it performs in Android yet. One thing to note is the mobile browser must support HTML 5. 🙂 If you try the links below on Android, drop me a comment whether or not it works and what Android version you are using.

Nearby Tweets Mobile: http://www.wswijaya.com/prototype/NearByTweetsMobile.php

Sencha Touch Version: 0.91 (public beta)