Mobile development has taken off, so many developers are choosing to go the mobile web route instead of writing the same application repeatedly for each different mobile platform. However, one of the things that you give up by "going web" is the application frameworks that make life easier for developers of native mobile applications. As a result, several web application frameworks are emerging. In this four-part series, we will look at four of these frameworks: SproutCore, Cappuccino, jQTouch, and Sencha Touch. We will compare the features of these frameworks and evaluate the pros and cons of using them to build a mobile web application.
In this article, we will create a simple mobile web application using the SproutCore framework. SproutCore makes heavy use of Ruby and Ruby Gems for code generation and its build system. This article uses Ruby version 1.8.7 and Gems 1.3.7. SproutCore is an all-JavaScript framework, with no server-side components and minimal HTML and CSS. Any web server will suffice. See Resources for links to these tools.
Are you looking to expand your development of iOS devices to reach non-iOS users? The combination of the mobile web and SproutCore might be the framework you seek. SproutCore brings a programming model inspired by the Cocoa framework to the web.
SproutCore is first and foremost a Model-View-Controller (MVC) framework for web applications. If you are a web developer, then you are probably familiar with MVC frameworks, like Struts or Ruby on Rails. SproutCore differs from these two, however, since they are server-side frameworks and SproutCore is a pure client-side framework. The M, the V, and the C all reside on the client side. This is actually a much more natural way for MVC to work; in fact, most desktop operating systems have offered similar MVC frameworks for decades because it is such a good fit.
SproutCore's architecture goes beyond a simple MVC framework. It provides a binding system that eliminates the need for much of the glue code, or code for taking data from models and using it in the views of the application. This kind of code is usually common in the controller layer of applications, but is entirely unnecessary in a SproutCore web application. It also provides an abstraction on top of data storage and retrieval. SproutCore offers a set of relatively lightweight widgets that work well in mobile applications. Its features let developers program at a higher level of abstraction than for web applications; it is not necessary to create and access HTML elements, manage CSS styles, or make XMLHttpRequests to remote servers. Instead, you'll work with a programming model very similar to desktop or native mobile application development, except in JavaScript.
Inside a SproutCore application
A SproutCore application at runtime typically consists of a single JavaScript file, a single CSS file, and a single HTML page. However, it begins as a number of JavaScript files that are compiled into optimized files ready to be deployed on any static web server. SproutCore comes with numerous tools, including build tools. For these to work, they must understand the structure of your source code. SproutCore relies on Convention over Configuration for this, and includes tools for generating the project structure and the typical files that you'll develop, such as models and controllers. The tools are built in Ruby and function very similarly to some of the popular Ruby on Rails framework tools. However, SproutCore is pure JavaScript. Since it lacks server-side components, there is no need for Ruby on your servers. You only need Ruby installed on your development machine to use the tools.
The focus of this article is on examining how SproutCore applications work, not how to use its tools. For that, a proper tutorial on SproutCore might be helpful. To explore SproutCore applications, take a look at what might be a typical mobile web application that you can develop using SproutCore. This application will provide a directory of employee contact information designed to be accessed from mobile devices. Begin by taking a look at this application, specifically its data access layer.
SproutCore development often takes a bottom-up approach, starting with describing the data model needed in the application and how it will be accessed from the server. Since JavaScript objects have no declared type information, it might seem counterintuitive to formally declare a model. However, some advantages to declaring a model are apparent. Listing 1 shows the data model for the application.
Listing 1. Employee data model
Intradir.Employee = SC.Record.extend({
firstName: SC.Record.attr(String, { isRequired: YES }),
lastName: SC.Record.attr(String, { isRequired: YES }),
phone: SC.Record.attr(String),
email: SC.Record.attr(String),
fullTime: SC.Record.attr(Boolean, { defaultValue: YES }),
fullName: function() {
return this.getEach('firstName', 'lastName').compact().join(' ');
}.property('firstName', 'lastName').cacheable()
}) ;
|
The code in Listing 1 shows the definition of the employee data model. The
application is known as Intradir, and the object is scoped to the
application. SproutCore models are known as records and typically extend
the SproutCore class SC.Record. Declare the
properties and types of your record. Listing 1 has five simple properties:
firstName, lastName, phone, email, and fullTime.
The first four are strings and the last is a Boolean value. To declare
this, use the SC.Record.attr helper function.
This helper function returns an instance of SC.RecordAttribute based on the type (String, Boolean, and
Number) passed to it. You must pass the type to this helper function, and
you can also pass optional attributes as well. In Listing 1, the isRequired option triggers the firstName and lastName
requirements. The fullTime property has a
default value triggered by the defaultValue
option.
Finally, notice that you have also declared a fullName property and have supplied a function for it. These
computed properties are very important in SproutCore. In this case, the
function retrieves the firstName and lastName properties and puts them into an
array-like structure that is then turned into a string using the join
function. That is the extent of the function declaration. On top of that,
declare that this computed property depends on the firstName and lastName properties
and that it is cacheable. This lets SproutCore cache the value after it is
computed for the first time, and use the cached value as long as it knows
that the firstName and lastName values have not changed. This kind of optimization
contributes to SproutCore's noteworthy performance on mobile devices.
SproutCore's Record API forms the basis of a lightweight object-relational mapping (ORM) technology. If you have multiple types of records that are related to each other, you can define one-to-many, one-to-one, and many-to-many relationships as well. To create, store, or retrieve records, you need SproutCore's Datastore API. SproutCore creates a Datastore for your application automatically. All you need to do is define your records and the data source for the Datastore, as shown in Listing 2.
Listing 2. Employee data source
Intradir.EmployeesDataSource = SC.DataSource.extend({
// ..........................................................
// QUERY SUPPORT
//
fetch: function(store, query) {
if (query == Intradir.EMPLOYEE_QUERY_ALL){
SC.Request.getUrl('/app/employees.json')
.set('isJSON',YES)
.notify(this, this._didFetchAllEmployees, {
query: query,
store: store
}).send();
return YES;
}
return NO;
},
_didFetchAllEmployees: function(response, params){
if (SC.$ok(response)){
store.loadRecords(Intradir.Employee, response.get('body'));
store.dataSourceDidFetchQuery(query);
} else {
store.dataSourceDidErrorQuery(query, response);
}
},
// ..........................................................
// RECORD SUPPORT
//
retrieveRecord: function(store, storeKey) {
// Not supported
return NO ; // return YES if you handled the storeKey
},
createRecord: function(store, storeKey) {
// Not supported
return NO ; // return YES if you handled the storeKey
},
updateRecord: function(store, storeKey) {
// Not supported
return NO ; // return YES if you handled the storeKey
},
destroyRecord: function(store, storeKey) {
// Not supported
return NO ; // return YES if you handled the storeKey
}
}) ;
|
The easiest way to create a data source is to use SproutCore's code
generation tool. It gives you an object that extends SC.DataSource and leaves you with five functions to implement:
fetch, retrieveRecord, createRecord, updateRecord, and destroyRecord. In the example in Listing 2, the fetch function
has been implemented; it is invoked whenever any kind of query is
executed. The fetch function looks for a specific query. Listing 3 shows this query and how it can be used.
Listing 3. Example of a SproutCore query
// Declare this in core.js so it can be used anywhere
Intradir.EMPLOYEE_QUERY_ALL = SC.Query.local(Intradir.Employee);
Intradir = SC.Application.create({
NAMESPACE: 'Intradir',
VERSION: '0.1.0',
// Create the store, and point it at our datasource
store: SC.Store.create().from('Intradir.EmployeesDataSource') }) ;
// Now in your application you can use the query
var directory = Intradir.find(Intradir.EMPLOYEE_QUERY_ALL);
|
The sample code in Listing 3 shows one of the simplest examples of the
kinds of queries that SproutCore supports. A local query like this one only
queries against what is stored locally in the datastore. However, Listing
2 shows that the datastore loads data from the server. To do this, use
SproutCore's Ajax utilities. This invocation is asynchronous, as it uses
XMLHttpRequest behind the scenes.
Therefore, the callback function, _didFetchAllEmployees, is invoked when the data returns from
the server. You can call this function anything you want, but we have
chosen to follow SproutCore's naming conventions. Prefix the function with
an underscore to denote that it is private, and use the Cocoa-style name
(didDoWhateverWeSaidWouldDo).
If you want to support other queries, add whatever logic you need to the fetch function. This can include remote queries, in which the query results come directly from the server. This usually involves dynamically forming a URL to request from the server, and typically adding callback functions to process the results. Similarly, the SproutCore data source API supports operations on individual records, though none of these have been implemented in this example. This typically depends on how your back-end is architected. With this data in the application, you simply need to display it and interact with it. Before you can do that, though, create a user interface that the data can be fed into.
JavaScript views, controllers, and key-value observers
View code in SproutCore is done in JavaScript and is written in a declarative style that is in many ways similar to HTML. The major difference is that SproutCore provides higher-level components that you only need to plug your data into to hook up event listeners (usually referred to as "observers"). For the application, load a simple table with employee data; the table will support sorting on any of its columns. Listing 4 shows the view code.
Listing 4. Creating a table view
Intradir.nameColumn = SC.TableColumn.create({
key: 'fullName',
label: 'Name',
width: 50
});
Intradir.emailColumn = SC.TableColumn.create({
key: 'email',
label: 'Email',
width: 50
});
Intradir.phoneColumn = SC.TableColumn.create({
key: 'phone',
label: 'Phone',
width: 50
});
Intradir.mainPage = SC.Page.design({
mainPane: SC.MainPane.design({
childViews: 'tableView'.w(),
tableView: SC.TableView.design({
layout: { left: 15, right: 15, top: 15, bottom: 15 },
backgroundColor: "white",
columns: [
Intradir.nameColumn,
Intradir.emailColumn,
Intradir.phoneColumn
],
contentBinding: 'Intradir.directoryController.arrangedObjects',
selectionBinding: 'Intradir.directoryController.selection',
selectOnMouseDown: YES,
exampleView: SC.TableRowView,
recordType: Intradir.Employee,
nameColumn: Intradir.nameColumn,
emailColumn: Intradir.emailColumn,
phoneColumn: Intradir.phoneColumn
})
})
});
|
This code is found in the /resources/main_page.js file. Think of this as
JSON data, even though you can invoke code as well. Minimize how much
imperative code (functions) is used in view scripts, and notice that the
bindings have been declared between the table that is created and a data
structure that is part of the directoryController. This is the controller that backs this
page. Listing 5 shows its code.
Listing 5. Controller for the main page
Intradir.directoryController = SC.ArrayController.create({
// nothing to see here!
}) ;
|
This controller shown in Listing 5 is very straightforward. The most
important thing about it is that it is an array controller, which means
that it extends SC.ArrayController. If you
return to the bindings declared in Listing 4, the properties that are
referenced are arrangedObjects and selection, which are defined in SC.ArrayController. This is a commonly used
controller that, as the name suggests, has an array that holds the model
data needed for the view. All you need to do is provide the data for the
controller and handle sorting. This step for the application's
initialization code is shown in Listing 6.
Listing 6. Application initialization code
Intradir.main = function main() {
Intradir.getPath('mainPage.mainPane').append();
var directory = Intradir.find(Intradir.EMPLOYEE_QUERY_ALL);
var dirController = Intradir.directoryController;
dirController.set('content', directory);
var tableView = Intradir.getPath('mainPage.mainPane.tableView');
// helper function
function handleSort(key, column){
var content = controller.get('content').sortProperty(key);
if (column.get('sortState') === SC.SORT_DESCENDING){
content = content.reverse();
}
dirController.set('content', content);
tableView.set('content',content);
tableView.displayDidChange();
tableView.awake();
}
// add observers
tableView.nameColumn.addObserver('sortState', function(){
handleSort("fullName", tableView.nameColumn);
});
tableView.emailColumn.addObserver('sortState', function(){
handleSort("email", tableView.emailColumn);
});
tableView.phoneColumn.addObserver('sortState', function(){
handleSort("phone", tableView.phoneColumn);
});
};
function main() { Intradir.main(); }
|
The data from the datastore is then used to populate the contents of the controller so that the controller has the array that backs it. If your only objective is to show the table, then you're done, because the rest of the code handles sorting. If not, define a helper function that you can use to handle sorting on each of the columns of the table. Finally, add observers to each of the three columns. Each observer looks for a sortState event, and then uses the helper function to sort the data and refresh the UI. This style of event handling is known key-value observing (KVO). The Cocoa framework uses this paradigm heavily; it has been brought to the world of JavaScript and web development by SproutCore.
The SproutCore website asks the question, "How do we build blazingly fast, desktop-class web applications?" and identifies SproutCore as the answer. However, this question not only fails to mention mobile web applications, but goes as far as to emphasize desktop-class web applications. Does that mean that SproutCore is only for web applications designed to only be accessed via desktop web browsers? To definitively answer this question, consider Hedwig, an example of using SproutCore for web applications designed for touch-enabled devices.
The Hedwig example shows you both the good and the bad aspects of using SproutCore for mobile applications. It is best to look at it on a large touch screen device like an iPad. SproutCore can assist with many essential aspects of mobile web development, such as dealing with touch events, orientation change, and dynamically sized controls. However, if you view it on smaller screened devices like an iPhone or an Android phone, then you will notice some things do not work as well. Figure 1 shows Hedwig on an iPad and on an iPhone.
Figure 1. Hedwig on iPad and iPhone in landscape
The problem here is mostly a function of the layout; the page is designed for a larger screen and absolutely positions some controls off the screen for a mobile device. Further, it uses a mobile-friendly feature, viewports, to make the page unscalable. This means that when the controls go off the mobile screen, you can't zoom out to access the control. However, making a page like this one friendly to smaller touch screens might only require minimal effort.
When considering using SproutCore for mobile development, remember that many of its UI components are not currently designed for smaller screens. The size or layout (or both) of these components might not be optimal for mobile screens. Consider how large (that is, how much JavaScript and CSS are needed) these components are, and how memory- and CPU-intensive they might be. Fortunately, SproutCore is very optimized in terms of rendering speed, so slower processors on mobile devices will not cause too much suffering on your part. Still, tread carefully when using larger, more complex components like the SC.TableView used in the example. Keep in mind that SproutCore includes helper methods for creating the raw HTML to be rendered by a custom component.
This introduction to SproutCore emphasized its framework and how it can be used for mobile web applications. On one hand, SproutCore provides a rich client-side MVC framework for creating web applications that heavily leverages JavaScript as a programming language. It uses binding to significantly reduce boilerplate code. It also provides abstractions on top of Ajax and encourages building all of the UI on the client, only going to the server for data. This architecture is perfect for mobile web applications, where you can use HTML5 technologies like the application cache. SproutCore also includes abstractions for touch events, which can lead to a much more interactive UI on mobile devices. On the other hand, SproutCore is not optimized for mobile devices, and not all of its UI components are well-suited for them, so you might have to build a lot of custom UI components that work much better on smartphones.
| Description | Name | Size | Download method |
|---|---|---|---|
| Intradir sample code | intradir.zip | 7KB | HTTP |
Information about download methods
Learn
- For more on SproutCore, peruse the official SproutCore
documentation.
- For more information about using Ajax in
mobile web applications, take a look at "Create Ajax applications for the mobile Web" (Michael Galpin,
developerWorks, May 2010).
- To learn more about using HTML5 features
for mobile web applications, read this five-part developerWorks series
about "Creating mobile Web applications with HTML 5" (Michael Galpin,
developerWorks, June 2010).
- Explore how you can leverage the features
of mobile browsers in "Android and iPhone browser wars, Part 1: WebKit to the rescue"
(Frank Ableson, developerWorks, December 2009).
- To learn even more about the different
ways to parse XML using the Android SDK, read "Working with XML on Android" (Michael Galpin, developerWorks,
June 2009).
- "New
elements in HTML 5" (Elliotte Rusty Harold, developerWorks, August
2007) teaches that HTML 5 is not just about JavaScript.
- Just becoming acquainted with Android?
"Introduction to Android development" (Frank Ableson,
developerWorks, May 2009) is a great place to start.
- The developerWorks Web development zone
specializes in articles covering various web-based solutions.
Get products and technologies
- The easiest way
to install SproutCore
is as a RubyGem.
- Download Ruby;
version 1.8.7 was used in this article.
- Download RubyGems; version 1.3.7 was
used in this article.
- Download IBM product
evaluation versions, and get your hands on application development
tools and middleware products from DB2, Lotus, Rational, Tivoli, and
WebSphere.
Discuss
- Create your My developerWorks profile today and setup a watch list on mobile web apps. Get connected and stay
connected with My developerWorks.
- Find other developerWorks members interested in web development.
- Share what you know: Join one of our developerWorks groups focused on web
topics.
- Roland Barcia talks about Web 2.0 and middleware in his blog.
- Follow developerWorks' members' shared bookmarks on web topics.
- Get answers quickly: Visit the Web 2.0 Apps forum.

Michael Galpin is an architect at eBay and a frequent contributor to developerWorks. He has spoken at various technical conferences, including JavaOne, EclipseCon, and AjaxWorld. To get a preview of his next project, follow @michaelg on Twitter.




