Mobile web application framework match-up, Part 1: Build mobile applications with SproutCore

Web applications have evolved significantly and are taking another leap forward with the advent of mobile web applications. Now more than ever, web applications are expected to provide a native experience—one that is on par with native mobile applications. Though mobile web browsers provide the capabilities to make this possible, web development is still primitive when compared to native application development. SproutCore is a web application framework that aims to make developing for the web similar to developing a native application for a particular device. Explore SproutCore and examine it as a framework for building mobile web applications.

Michael Galpin, Software architect, eBay

Michael Galpin's photoMichael 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.



19 October 2010

Also available in Chinese Russian Japanese

About this series

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.


Prerequisites

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.


An overview of SproutCore

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.

Data, models, and stores

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.


SproutCore and mobile

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
Hedwig looks better on large screened devices like the iPad than on the smaller iPhone

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.


Conclusion

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.


Download

DescriptionNameSize
Intradir sample codeintradir.zip7KB

Resources

Learn

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Web development on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Web development, Open source, Mobile development
ArticleID=551320
ArticleTitle=Mobile web application framework match-up, Part 1: Build mobile applications with SproutCore
publish-date=10192010