PhoneGap By Example
上QQ阅读APP看书,第一时间看更新

Understanding the basic application structure

We already looked at the filesystem structure. It is now time to understand how these files are tied together. You also need to understand what an interaction between code parts is.

Understanding the basic application structure

Ext.application is the starting point in our application. As we noted earlier, it might contain the app name, and references to all the models, views, controllers, profiles, and stores. These are explained as follows:

  • Profiles: These allow us to customize the application's UI for handsets and tablets
  • Models: These represent a type of data in our application
  • Views: These actually present data in our application within Sencha Touch components
  • Controllers: These handle interactions with our application by listening for user's taps and swipes
  • Stores: These store our data, which we display in grids and other elements

You can see the single instance of Ext.application in the generated www/app.js file:

Ext.application({
    name: 'Travelly',
    views: [ 'Main' ],
    // ...
    launch: function() {
      Ext.fly('appLoadingIndicator').destroy();
      Ext.Viewport.add(Ext.create('Travelly.view.Main'));
    }
    // ...
});

Where:

  • Travelly: This is the name of our generated application. We will use it as a global namespace in different places of our application, for example, Travelly.controllers.Main, Travelly.model.Main, Travelly.view.Main.
  • Main: This is the name of our one and only view in the application.
  • Launch: This is a method called once the application loader has loaded all the required dependencies.
  • .fly: This is Sencha's method used to make a one-time reference to DOM. It is taking HTML element on the index.html page with id="appLoadingIndicator" and destroys it.
  • Viewport.add: This inserts Main view into index.html body.

Tip

Sencha Touch is built using lessons learned from Ext JS (http://www.sencha.com/products/extjs). That is why, in our application, you can see the Ext prefix in many places.

The current file structure inside the app folder looks like this:

├── controller
├── form
├── model
├── profile
├── store
└── view
    └── Main.js

It simply becomes clear that in the controller folder, we should place our controllers, in view our views, and so on. Now, all our folders, except view, are empty. Let's fix it and create several items we will need for our application.

Getting familiar with the Sencha Touch view

Our application has already a defined view in the view/Main.js file:

Ext.define('Travelly.view.Main', {
    extend: 'Ext.tab.Panel',
    xtype: 'main',
    requires: [ 'Ext.TitleBar', 'Ext.Button' ],
    config: {
      tabBarPosition: 'bottom',
      items: [
        {
          title: 'Welcome',
          iconCls: 'home'
          // ...
        },
        {
          title: 'Get Started',
          iconCls: 'action'
          // ...
        }
      ]
    }
});

Let's break the preceding code down.

The Ext.define function helps us define a class named Main in the Travelly/view namespace. All view components are placed within this namespace as per Sencha Touch MVC standards.

The extend keyword specifies that the Main class is the subclass of Ext.tab.Panel. So, the Main class inherits the base configuration and implementation of the Ext.tab.Panel class.

The xtype keyword is used to instantiate the class.

The requires property is used because we use a button in our items array. We indicate the new view to require the Ext.Button class. At the moment, the dynamic loading system does not recognize classes specified by xtype, so we need to define the dependency manually.

The Config keyword helps us initialize the variables/components used in that particular class. In this example, we should initialize the tab panel view with tabs and panels. Tab panels are a great way to allow the user to switch between several pages that are all full screen.

The content of Items in the Main view currently has two items with title and iconCls. The tabBarPosition property defines where the tab bar is placed, and in our case, it is at the bottom. With title, we can set text on a button in the tab panel, and with iconCls, we can show the predefined icon. Let's add a button to one of this tab panels.

To add a new element to any container (in our case, it is tab panel), it is enough to assign an array of objects to items property. Let's do this with our first tab panel:

{
   title: 'Welcome',
   iconCls: 'home',

   items: [
       {
           xtype: 'button',
           text: 'My button',
           id: 'myButton'
       }
   ]
}

You can see that we added some text and assigned id for the button. This will help us handle button events in the controller. However, we can already handle it right in the view. Here is an example:

xtype: 'button',
text: 'My button',
id: 'myButton',
handler: function() {
   alert('My button has been clicked!');
}

Once the user has tapped on the button, we will see an alert message right away.

And we are done! We just successfully added the Sencha Touch button component to the first tab of our application. We should remember that our application already has the Ext.Viewport.add(Ext.create('Travelly.view.Main')) code to add our view on the page.

Now, let's create some controller and handle user interaction on the Main view.

Creating the Sencha Touch controller

Controllers listen for events fired by the UI and take actions based on the event. Using controllers helps keep your code clean and readable, and separates the view logic from the control logic.

There are two ways to create the controller: using Sencha Cmd or manually. To create the controller with Sencha Cmd, it is enough to execute the following command:

$ sencha generate controller Main

It generates the controllers/Main.js file for us. It is generic and has minimum content:

Ext.define('Travelly.controller.Main', {
    extend: 'Ext.app.Controller',
    config: {
      refs: {},
      control: {}
    },
    launch: function(app) {}
});

Our controller is a subclass of Ext.app.Controller, which is instantiated only once by the application that loaded it. At any time, there is only one instance of each controller. To instantiate the controller automatically, we have to add it into the controllers configuration section in the application in the following way:

controllers: [ 'Main' ]

You can see the launch method. This method is triggered automatically for every controller, every time the application starts. If you do not need it, simply remove this method from the controller.

The config section is different from the view's section, but it contains two important properties: refs and controls.

The refs property is an easy way to find components on the page. The control property is similar to the ref's config property, but it allows us to define event handlers. Here is an example:

config: {
   refs: {
       myButton: '#myButton'
   },
   control: {
       myButton: {
           tap: 'doMyButtonTap'
       }
   }
},
doMyButtonTap: function() {
   alert('My button has been clicked!');
}

In the refs section, we are using ComponentQuery to find our button on the page by id. Whenever a button of this type fires its tap event, our controller's doMyButtonTap function is called.

This is mostly what a controller does—listens for events that fire (usually by the UI) and initiates some action.

Tip

Ext.ComponentQuery lets us search of components within Ext.ComponentManager (globally) or a specific Ext.Container on the document with syntax similar to a CSS selector.

Using store

When web developers think about storing something for the user, they try to upload data to the server and store it there. However, it is a huge issue in mobile web and hybrid applications development when you do not have stable Internet connection. Sometimes, a device can go offline and go online only in several days. We have to stop building apps with a desktop mindset where we have permanent, fast connectivity. Offline technologies don't just give us sites that work offline; they improve performance and security by minimizing the need for cookies, HTTP, and file uploads. They also opens up new possibilities for better user experience.

HTML5 allows us to make the approach real. Now, it is possible to store data on client side in WebStorage, IndexedDB, and Web SQL Database, which are explained here.

  • LocalStorage: This is also known as web storage, simple storage, or by its alternate session storage interface. This API provides synchronous key/value pair storage.
  • Web SQL Database: This offers more full-featured database tables accessed via SQL queries.
  • IndexedDB: This offers more features than LocalStorage, but fewer than Web SQL.

However, there is a limitation for using these client-side storages with the PhoneGap application. Here is a list of mobile platforms supporting different storages:

  • LocalStorage
    • All supported by PhoneGap platforms
  • WebSQL
    • Android
    • BlackBerry 10
    • iOS
    • Tizen
  • IndexedDB
    • BlackBerry 10
    • Windows Phone 8
    • Windows 8

Tip

You can learn more about browser storage support at http://www.html5rocks.com/en/tutorials/offline/quota-research/.

Now, it is clear what storages we can use with PhoneGap. It is great news that Sencha Touch already has an abstraction for the storage—Ext.data.Store.

Before looking deeper into Ext.data.Store, let's get familiar with Sencha Touch models. These models are very important, because they are the main components of the store.

The Sencha Touch model

This model represents objects of business models from our application. For example, we want to store metadata for pictures we capture with camera. In our Travelly application, we can define model Picture. Let's generate it as we did for controller:

$ sencha generate model Picture id:int,url:string,title:string,lon:string,lat:string

Where:

  • model: This is an attribute for the generate command to generate the model
  • Picture: This is a name of our model to generate
  • id:int,url:string,title:string,lon:string,lat:strin: These are fields for our model with their types

In the model folder, we should see the Picture.js file:

Ext.define('Travelly.model.Picture', {
    extend: 'Ext.data.Model',
    config: {
      fields: [
        { name: 'id', type: 'int' },
        { name: 'url', type: 'string' },
        { name: 'title', type: 'string' },
        { name: 'lon', type: 'string' },
        { name: 'lat', type: 'string' }
      ]
    }
});

Where I have used the following components in the file:

  • id as a unique identifier for the picture
  • url as a link to the picture
  • title of the picture
  • lon as a longitude coordinate of the place where the picture was taken
  • lat as a latitude coordinate of the place where the picture was taken

To work with this model in our application, we should add Travelly.model.Picture to the requires section. Usually, we do this in controllers or stores. To create an instance of this class, use the following lines of code:

var picture = Ext.create('Travelly.model.Picture', {
    id: 1,
    url: 'http://myurl.my/somepicture.jpg',
    title: 'My picture',
    lat: '50.450783',
    lon: '30.523035'
});

Where:

  • Ext.create: This instantiates a class by either full name, alias, or alternate name
  • Travelly.model.Picture: This is the name of our model class

We can access model object values by property names. For example, picture.get('title') will return the title of our picture.

The Sencha Touch store

Let's create a data store and map it to the preceding model. There is no Sencha Cmd command for it. So, we will do this manually:

Ext.define('Travelly.store.Pictures',{
    extend:'Ext.data.Store',
    config:{
      model:'Travelly.model.Picture', 
      autoLoad:true,
      data:[
        {
          id: 1,
          url: 'http://myurl.my/somepicture1.jpg',
          title: 'My picture 1',
          lat: '50.450783',
          lon: '30.523035'
          },
          {
            id: 2,
            url: 'http://myurl.my/somepicture2.jpg',
            title: 'My picture 2',
            lat: '50.450783',
            lon: '30.523035'
          }
        ],
        proxy:{
        type:'localstorage'
      }
    }
});

Where:

  • autoLoad: true: This means the store's load method is automatically called after creation. We do not need to call the .load method before working with data.
  • data: This is a inline data based on our model.
  • proxy: This is used to load and save data. We used LocalStorage.

The Sencha store provides an easy way to add and retrieve data from the store:

var pictureStore = Ext.getStore('Pictures');
pictureStore.add(picture);
pictureStore.sync();
var foundPic = pictureStore.findRecord('id', 1);

Where:

  • Ext.getStore gets the instance of the Pictures store that is already defined and initialized
  • pictureStore.add adds our early created picture object to the store
  • pictureStore.sync syncs our in-memory store data to LocalStorage
  • pictureStore.findRecord finds and retrieves the Picture model instance from the in-memory storage

The Sencha Touch proxy

There are two types of proxies that could be used: client and server.

These are the client proxies:

  • LocalStorageProxy: This stores data in LocalStorage
  • MemoryProxy: This stores data in memory only

These are the server proxies:

  • Ajax: This is used to interact with a server on the same domain
  • JsonP: This uses JSONP to send requests to a server on a different domain

We will use server proxies when we integrate our application with RESTful service on Node.js.

Environment detection

Very often, when we run the application, we need to know on which platform we are running it. With Sencha Touch, we can detect this information:

  • Operating system: Ext.os.is.[iOS, Android, MacOS, Windows], and so on
  • Device: Ext.os.is.[iPhone, iPad, iPod] and so on
  • Browser: Ext.browser.is.[Safari, Chrome, Firefox] and so on.
  • Browser's features: Ext.feature.has.[Canvas, Css3dTransforms] and so on.

In our case, it is very helpful to use environment detection when we develop. We can detect whether it's desktop browser or mobile device, and run or avoid device-specific functions. Alternatively, we can run different code for Android and iOS.

Creating device profiles

Very often, you need to make the application behave differently on different devices. We can separate code execution by form factor or by operating system. It doesn't matter which we use.

Let's imagine that we need some specific functionality running only on Android tablet, and on all the other devices, we would like to keep it generic.

To generate a specific profile, let's execute the Sencha Cmd command:

$ sencha generate profile AndroidTablet

It generates the profile/AndroidTablet.js file for us. It is pretty basic, and I modified it to look like this:

Ext.define('Travelly.profile.AndroidTablet', {
    extend: 'Ext.app.Profile',
    config: {
      views: [],
      models: [],
      stores: [],
      controllers: [ 'Main' ]
    },
    isActive: function(app) {
      return Ext.os.deviceType == 'Tablet' && Ext.os.is.Android;
    }
});

Once the profiles have been loaded, their isActive functions are called in turn. If these functions return true, the application loads all of its dependencies—the models, views, controllers, and other classes.

Following the launch process

Each application, profile, or controller can define the launch function. However, they don't have to.

Here is the order of functions called after the application starts:

  1. Controller's init
  2. Profile's launch
  3. Application's launch
  4. Controller's launch

Tip

Only the active profile has its launch function called.

The init method is called by the controller's application to initialize the controller. Here, we can place any pre-launch logic. At this stage, we do not have UI ready.

By the time the chain of calls reaches the controller's launch, we have already prepared the UI, because mainly, all UIs are created by the profile's or application's launch functions.

The UI and theming

While Sencha Touch initially favored an iOS-styled interface, its main theme is not platform oriented and does not mimic any of the mobile OSes entirely. Nonetheless, iOS, Android, and Windows lookalike themes are shipped with the entire package. Sencha Touch utilizes the SASS approach to its fullest extent. We can quickly restyle a look and feel for our needs. I do not really like the default Sencha Touch theme. So, let's try to change it to iOS cupertino.

Let's do this using these steps:

  1. Run the compass watch command. Sencha Cmd does it for us, with the following command:
    $ sencha app watch
    
  2. Open up the /resources/sass/app.scss file in the text editor
  3. Save a copy of the app.scss file and rename it to cupertino.scss
  4. Modify cupertino.scss so that it looks like this:
    @import 'sencha-touch/cupertino';
    @import 'sencha-touch/cupertino/all';
  5. Link our newly generated cupertino.css file in the app.json file. Let's make the following changes to it:
    "css": [
       {
           "path": "resources/css/cupertino.css",
           "update": "delta"
       }
    ]

And we are done! When we started the $ sencha app watch command, you might have realized that it showed the localhost URL address to test application in output. In my case, it was presented in the following way:

$ sencha web start
Sencha Cmd v5.1.0.26
[INF] Mapping http://localhost:1841/ to ....
[INF] ---------------------------------------------------------------
[INF] Starting web server at : http://localhost:1841
[INF] ---------------------------------------------------------------

So I opened the http://localhost:1841 URL in the browser and was able to see the application. Similarly, you can use the $ sencha web start command. However, it is not watching for scss changes. In the upcoming chapters, we will often use the simple Sencha web server for our testing and debugging needs.