Creating Mobile Apps with Appcelerator Titanium
上QQ阅读APP看书,第一时间看更新

Let's get this show on the road

We will now go through all the steps required to build our application.

The database

Before we dive into the application's look and feel, we need to define how our application will store the information. Titanium provides us with an embedded relational database management system (RDMS) called SQLite. It is a widely used open source database, which has very low memory consumption, making it the natural choice for mobile development where resources are limited.

Like most relational databases, the information is stored in tables and is accessed using the SQL query language. There are plenty of books and online articles covering all the intricacies of relational databases and SQL. So, we will not go into much detail on this subject, but will cover the basics that are necessary to store, update, and delete information from a database.

Defining the structure

Every time we need to store information in a database, we need to define a structure. For our application, the structure is pretty straightforward. Each task needs a name and some sort of a flag defining whether the task is complete or not. So, in our case, a single table would perfectly meet our needs. We shall name it TODO_ITEMS to keep things meaningful.

Our table's structure is as follows:

Tip

For those of you who are unfamiliar with database tables, think of them as spreadsheets, where each column maps to a task attribute, and where each row is a task.

Implementing our model

Now that we have determined the structure of our database, we can now implement the said structure into our code. All database operations are contained in the Titanium.Database namespace and provide a lot of functionalities that will be covered throughout this chapter.

After opening the app.js file and deleting all of its content, the next thing we want to do is to open (or create if it is still not present on the device) our database file using the open function with the filename as a parameter. This will return a reference to the opened database. If the database does not exist, it will create an empty database file and return a reference to this opened database. We will then store its reference in a variable named db for later use in our code as follows:

var db = Ti.Database.open('todo.sqlite');

Once we have access to our database, we want to make sure that our database structure is present. We then execute the following SQL statement to create the TODO_ITEMS table if it is not already present:

db.execute('CREATE TABLE IF NOT EXISTS TODO_ITEMS (ID INTEGER
  PRIMARY KEY AUTOINCREMENT, NAME TEXT, IS_COMPLETE INTEGER)');

The preceding statement will not raise any error or exception since it contains the IF NOT EXIST clause. So there is no extra code needed; by doing this right at the beginning of our code (even before declaring the UI), we are sure that the structure is well in place right from the start.

Note

While the table creation statement could work without the IF NOT EXIST clause, there will be no protection if the statement fails, resulting in data loss.

The user interface

Having previously defined our user interface, we are now able to implement the necessary components of our application. While not yet interactive, it will provide us with a solid baseline to build upon.

As with every single-window application, we create a standard window using Ti.UI.createWindow with a white background and To Do List as its title. Since we want to add additional controls to this newly created window, we store its reference in the win variable as follows:

var win = Ti.UI.createWindow({
  backgroundColor: '#ffffff',
  title: 'To Do List'
});
The header view

Our header view is basically a container for our controls that will sit on top of the screen. It will have a height of 50dp, spanning the whole width of the screen and will have a blue background. It will also have a horizontal layout, since we do not know how large the screens of some devices are:

var headerView = Ti.UI.createView({
  top: 0
  height: '50dp',
  width: '100%',
  backgroundColor: '#447294',
  layout: 'horizontal'
});

The header view contains two controls. A text field that is used to enter the task's name and a button to add a new task to the database. The text field will span 75 percent of the screen's width and will display a hint text when the field is empty, providing a better user experience:

var txtTaskName = Ti.UI.createTextField({
  left: 15,
  width: '75%',
  height: Ti.UI.FILL,
  hintText: 'Enter New Task Name',
  borderColor: '#000000',
  backgroundColor: '#ffffff',
 borderStyle: Ti.UI.INPUT_BORDERSTYLE_ROUNDED
});
headerView.add(txtTaskName);

The button, while square dimensioned, will use the backgroundImage property in order to override the button's default rendering as follows:

var btnAdd = Ti.UI.createButton({
  backgroundImage: 'add_button.png',
  left: 15,
  height: '45dp',
  width: '45dp'
});
headerView.add(btnAdd);

We will then add the header to our application window:

win.add(headerView);
The tasklist

The tasklist will use the TableView component to display the items contained in the database. As for the header, we will use a View container. Its top position will be at 50dp, which is precisely the height of the header view. So if we were to change the header's height, we would have to update this value as well:

var taskView = Ti.UI.createView({
  top: '50dp',
  width: '100%',
  backgroundColor: '#eeeeee'
});
Note

Notice that we did not specify any height for this view. The reason being we already specified the height of the header view (and the footer view).Therefore, the middle view will stretch in order to occupy all the remaining space on the screen.

The table view is created using a limited set of properties since there is no data (yet) to assign to it. We want it to grow in order to fill its parent view; we achieve this behavior using the Ti.UI.FILL constant. We will also change the default separatorColor property (the thin line between each row):

var taskList = Ti.UI.createTableView({
  width: Ti.UI.FILL,
  height: Ti.UI.FILL,
  separatorColor: '#447294'
});

We then add the table view to its container and the container to the main window:

taskView.add(taskList);
win.add(taskView);
The button bar

A view similar to the header will be used for the button bar, but instead, at the bottom of the screen. It will share most of the same properties as the header view, in terms of dimensions and background color. Of course, since we want it to stay at the bottom of the screen, we will do so by setting the bottom property to 0:

var buttonBar = Ti.UI.createView({
  height: '50dp',
  width: '100%',
  backgroundColor: '#447294',
  bottom: 0
});

The next control is a switch that will allow us to hide tasks that are marked as completed. This can be very useful when the user wants to have a glance at what is left to do. We can create it using the Ti.UI.createSwitch function, and we want it to display which filter is applied depending on its status (all the tasks when it is on, and only active tasks when it is off). We also want to set its default value; in our case, we want to show all the tasks by default:

var basicSwitch = Ti.UI.createSwitch({
  value: true,
  left: 5,
  titleOn: 'All',
  titleOff: 'Active'
});

The second control is a regular button that will be used to clear all the completed tasks from the database:

var btnClearComplete = Ti.UI.createButton({
  title: 'Clear Complete',
  right: 5
});

We then add both the controls to the button bar, and then add the newly created bar to the main window:

buttonBar.add(basicSwitch);
buttonBar.add(btnClearComplete);
win.add(buttonBar);
win.open();
Let's have a look

Let's take our newest application for a spin by clicking on the Run button from the App Explorer tab.

The following screenshot depicts what we see on our first run:

Of course, there are no tasks in the list, and the controls have no code behind them. But this is usually a good time to make that sure everything fits nicely and reacts in the right way in terms of the UI. Does the onscreen keyboard mess up the user experience when it is displayed? What about rotation?

Just keep in mind that, at this stage, it is easier to edit or move around the code when there is little to no impact on the application's logic.

Developing a better switch for iOS

For those of you who have tested the code using the iPhone simulator, you may have noticed there was something odd about the switch component. The titleOn and titleOff properties don't seem to reflect on the iPhone version. This is because on iOS, a switch is only displayed as an on/off switch without any text associated with it.

In the following figure we can see the difference in the implementation between the two versions:

Since the default behaviour on iPhone doesn't suit our needs, we must find a way to get around this problem. Luckily, Titanium allows us to run sections of code, depending on the platform on which the application is installed.

iOS has a component called the tabbed bar. It is a button bar that maintains a state by having one button pressed at a time. We can use such a component with two buttons, each having a different title. While the look will be different, the functionality will remain the same. On a positive note, it will have a more native look and feel, since it adheres more to the platform's guidelines.

We must then modify our switch declaration. Instead, we simply declare it and will assign it with proper implementation later. This is made possible thanks to the fact that JavaScript is a dynamic language. We can totally create a variable and assign different values to it, depending on the context:

var basicSwitch;

If we are running on the iOS platform, we create a new tabbed bar using the createTabbedBar function. First, we set its two button labels using an array of string. We want it to have the same background color as the button bar and will give it a special style, which will give it a more compact look. We also want the All button to be selected by default, the first element of the labels array, by setting the index property to zero.

If we are not running on the iOS platform, we use the previously created switch, just as we did in the first version of the code:

if (!Ti.Android) {
  basicSwitch = Ti.UI.iOS.createTabbedBar({
    labels: ['All', 'Active'],
    left: 5,
    backgroundColor: buttonBar.backgroundColor,
    style: Ti.UI.iPhone.SystemButtonStyle.BAR,
    index: 0
  });
} else {
  basicSwitch = Ti.UI.createSwitch({
    value: true,
    left: 5,
    titleOn: 'All',
    titleOff: 'Active'
  });
}
Tip

We could have checked whether we were running iOS by checking whether the Ti.Platform.name property matched the iPhone OS character string. But this would imply an additional verification if we are running on an iPad. Simply checking whether the Ti.Android instance is present is a neat little trick to determine whether we are running Android without performing any string comparison.

We can now see a new control being used when testing on the iPhone simulator. This gives us a look and feel that is more consistent with the platform.

While checking for the platform to run the specific piece of code is useful for small instances such as this one, it can make the code much harder to read and difficult to maintain when faced by large chunks of code that are radically different between platforms.

Titanium addresses this problem by giving us the possibility to load an entire JavaScript file depending on the platform. In the Resources directory, there is a directory for each target platform; we can then use those same directories to provide a different implementation of certain features. As long as the files share the exact same name, Titanium will load the right file.

Here is what it would look like if we were to extract the button bar into its own source file, each having its own implementation. Thus, we would completely avoid the need to check for the platform's name in the code:

What is important to notice here is that both files must have the exact same name (matching the same case).

Note

All platform-specific files (JavaScript, images, media, and so on) are used at build time. The way it works is quite simple. When you build your project, Titanium takes all the files contained in your target directory and copies them into your Resources directory. If there is already a file with a similar name, it will overwrite it with the target-specific one. This is a great way to have what you could call a default behaviour for all the platforms, but have a specific implementation for one target for instance.

Adding a new task

With the database set up and all the visuals in place, it is time to add new tasks to our database. We will create a function that inserts a new record into the database using the task's name passed as a parameter. The execute function takes two parameters in this case. The first one is the SQL statement, and it is pretty close to English when you look at it closely.

Tip

You could read a SQL INSERT statement that is pretty much similar to the following:

Insert into the TODO_ITEMS table using the following two columns (NAME and IS_COMPLETE), and the following values: one we don't know yet (hence the question mark), and the second one is zero (for IS_COMPLETE=false, since it is a brand new task).

The second parameter is what will replace the question mark in your SQL statement. For every question mark in the SQL statement, there must be a matching parameter in the function call.

Once the task is inserted into the database, we set the text field's value to an empty string to allow the user to type a new one. We also hide the onscreen keyboard if it is displayed using the blur function:

function addTask(name) {
  db.execute('INSERT INTO TODO_ITEMS (NAME, IS_COMPLETE)
VALUES (?, 0)', name);

  txtTaskName.value = '';
  txtTaskName.blur();
}

We call the addTask function with the text field's value as a parameter when the user clicks on the Add button:

btnAdd.addEventListener('click', function(e) {
  addTask(txtTaskName.value);
});

We also want to automatically create a new task when the user clicks on the Return key on the onscreen keyboard. To do this, we simply fire the click event from the Add button, which avoids having to copy and paste the code from the click event handler:

txtTaskName.addEventListener('return', function() {
  btnAdd.fireEvent('click');
});

Now, if we create a few tasks using the application, and if we query the database, here is what we would get:

sqlite> select * from todo;
    id      name                is_complete
    ----    -----------------   -----------
    1       Dry Cleaner         0
    2       Call Bob            0
    3       Pickup Parcel       0
    4       Buy Milk            0
    5       Walk the dog        0
    6       Balance Checkbook   0

This is good because we know our records are inserted correctly.

Listing all the existing tasks

Now that our application can insert tasks successfully into the database, we can now move on to read those same tasks and display them to the user. To do this, we will create a function called refreshTaskList that will do just that.

First we need to retrieve all the tasks from the database using a SELECT statement. The execute function returns a list of rows (also known as a ResultSet) that we will reference as the rows variable:

function refreshTaskList() {
  var rows = db.execute('SELECT * FROM TODO_ITEMS');
  var data = [];

We then need to loop through each row in order to extract the needed information. We can use the isValidRow function to determine whether the current row is valid.

Then, we extract the information using the fieldByName function and the column's name as a parameter. Since this function can return either a string, a number, or even binary data, we must tailor the data to fit our needs (padding with two empty strings for text on converting 1 and 0 to true or false).

Using the extracted information, we create an object containing all the attributes needed to create a table view row with an extra custom property for the ID. We must not forget to set the className property to avoid any drop in performance.

Since the ResultSet object is not an array, our code has to move to the next record, or else we would be caught in an infinite loop. Once we pass through all the records, we exit the loop and set the newly created array to the table view:

  while (rows.isValidRow()) {
    var isComplete = rows.fieldByName('IS_COMPLETE');

    data.push({
      title: '' + rows.fieldByName('NAME') + '',
      hasCheck: (isComplete===1) ? true : false,
      id: rows.fieldByName('ID'),
      color: '#153450',
      className: 'task'
    });

    rows.next();
  };

  taskList.setData(data);
}
Note

Some of you may have noticed the use of the strict equal (===) operator when setting the hasCheck property. This is different from using the equal (==) operator. While they might seem alike and even, sometimes, share the same behaviour, they have one major difference. Equal returns true if the operands are equal. But strict equal returns true if the operands are equal and of the same type. This is very useful in cases where you need to be absolutely sure that the two values are the same.

Before executing the code

While it is true that we have a function that will fill our tasklist, running the code in its present state will not show anything. The reason for that is because the function is not called from anywhere in the code. Therefore, we must add the following line:

refreshTaskList();

The preceding line must be added to the following locations:

  • Right before the main window opens (this will load the list when the application launches)
  • At the very end of the addTask function, since we want to refresh the list once a new task is added to the database

Now, if we run the application, we can see all our previously saved tasks displayed in the table view, as shown in the following screenshot:

Marking a task as completed

To mark a task as completed, we will need to add an event listener for the click event on the table view. From there, we can exactly retrieve which element from the list has been selected, as well as its properties from e.rowData.

taskList.addEventListener('click', function(e) {
  var todoItem = e.rowData;

Then, we determine whether the task is marked as completed using a very straightforward toggle mechanism. If the user selects an incomplete task, we set it as completed. If the task is marked as completed, we simply set its status as incomplete, as shown in the following code:

  var isComplete = (todoItem.hasCheck ? 0 : 1);

We will then update the database record's IS_COMPLETE column. To achieve this, we will use the UPDATE SQL statement with two parameters (the ID and whether it is completed or not):

  db.execute('UPDATE TODO_ITEMS SET IS_COMPLETE=? WHERE ID=?',
      isComplete, todoItem.id);

Once the database has been updated, we refresh the whole table view:

refreshTaskList();
});

If we run our application now, we can toggle between tasks depending on their status by clicking on the desired row.

Filtering out completed tasks

When working with large To-do lists, it is often considered useful to show only active tasks. This will make the tasklist more readable and easier to navigate. To do this, we will create a function named toggleAllTasks with a Boolean value as a parameter to determine whether we filter the list or not.

If the user chooses to show all the items, we refresh the tasklist. If the user chooses to show only active tasks, we will need to loop through the table view's dataset. For this, it is important to understand that all the table view rows are contained in table view sections. Even when we don't define any section while populating the table view, there is always one created by default. So, we need to get a reference to this first (invisible) section by accessing the first value contained in the data property.

Once we have a hold of the section, we can loop through the rows array that contains every row shown on the screen. Looping through each row, we check whether the row is completed or not using the hasCheck property. If it is completed, we delete the row by calling the deleteRow function by specifying the row index we want to delete:

function toggleAllTasks(showAll) {
  if (showAll) {
    refreshTaskList();
  } else {
    var section = taskList.data[0];

    for (var i = 0; i < section.rowCount; i++) {
      var row = section.rows[i];

      if (row.hasCheck) {
        taskList.deleteRow(i);
      }
    }
  }
}
Activating the filter

Now that we have our function that can toggle between showing all the tasks and showing only active tasks, we need to link it to our switch (or tabbed bar on the iPhone).

For iPhone, we must add an event listener for the click event on the tabbed bar. Inside this listener, we call the toggleAllTasks function with the parameter depending on which button was selected. The same event listener must be declared right after the tabbed bar is created.

For Android, we must add an event listener for the change event on the switch. Inside this listener, we call the toggleAllTasks function with the parameter depending on the switch value. Similar to the tabbed bar listener, this listener must be declared right after the switch is created:

if (Ti.Platform.name === 'iPhone OS') {
  // createTabbedBar code unchanged...
  basicSwitch.addEventListener('click', function(e) {
    toggleAllTasks(e.index === 0);
  });
} else {
  // createSwitch code unchanged...
  basicSwitch.addEventListener('change', function(e) {
    toggleAllTasks(e.value === true);
  });
}

Filtering the tasklist to show only the active tasks makes the list easier to read and provides a better user experience, as shown in the following screenshot:

Deleting the completed tasks

Deleting all the completed tasks from the database can be done using a single DELETE statement. Here, the filter is based on records that have the IS_COMPLETE column set to 1 (converted from true earlier).

Once all the completed tasks are deleted from the database, we refresh the table view:

btnClearComplete.addEventListener('click', function(e) {
  db.execute('DELETE FROM TODO_ITEMS WHERE IS_COMPLETE = 1;');
  refreshTaskList();
});

Close the door, you're letting the heat out

Every time you call the open function on a SQLite database, the system allocates resources into memory in order to perform its operations. In order for these resources to be freed from memory, we must call the close function. Since we open the database when the application launches, it makes perfect sense to close it when the application is closed.

One good way to achieve this is to add an event listener to the main window's close event:

win.addEventListener('close', function() {
  db.close();
});