9781449390501
Android_Adapters.html

Chapter 10. Lists and Adapters

If you're new to the Android mobile operating system, Learning Android is the perfect way to master the fundamentals. This gentle introduction shows you how to use Android's basic building blocks to develop user interfaces, store data, and more. Buy the print book or ebook.

In this chapter, you will learn how to create selection widgets, such as a ListView. But this isn’t just a chapter about user interface elements. We are continuing to deepen our understanding of data from the previous chapter by learning how to read data from the database of statuses and simply output it on the screen as scrollable text first. You will then learn about Adapters in order to connect your database directly with the list. You will create a custom adapter in order to be able to implement some additional functionality. You will link this new activity with your main activity so that the user can both post and read tweets.

By the end of this chapter, your app will be able to post new tweets, as well as pull them from Twitter, store them in local database, and let the user read the statuses in a nice and efficient UI. At that point, your app will have three activities and a service.

TimelineActivity

We’re going to create a new activity called TimelineActivity to display all the statuses from our friends. It pulls the data from the database and displays it on the screen. Initially, we do not have a lot of data in the database, but as we keep on using the application, the amount of statuses we have may explode. Our application needs to account for that.

We are going to build this activity in couple of steps, at each point keeping the application whole and complete as we make improvements.

  1. The first iteration of TimelineActivity uses a TextView to display all the output from the database. Since there may be quite a bit of data, we will use ScrollView to wrap our large amount of text in so that there are scroll bars on provided.
  2. The second iteration uses the much more scalable and efficient ListView and Adapter approach. In this step, you will learn how Adapters and Lists work.
  3. Finally, we will create a custom Adapter to handle some additional business logic. At this point, we are going under the hood of an adapter and adding custom processing. You’ll understand the purpose and usage of adapters better after this exercise.

Basic TimelineActivity Layout

In this first iteration, we are creating a new layout for the TimelineActivity. This layout initially uses a TextView to display all the data that we have in the database. This is fine initially when we don’t have too many statuses to show.

Introducing ScrollView

Since it’s unlikely that all our data will fit on a single page, we need a way to scroll the text. To do that, we use ‘ScrollView`. ScrollView is like a window that displays part of a larger component that takes more space than the screen provides. It does that by providing convenient scroll bars as as needed. To make some potentially large views scrollable, you wrap them with this ScrollView. For example, we have a printout of many friends’ statuses in form of a TextView. This TextView could become large as there are many statuses. In order to make it scrollable on a small screen, we put it into a ScrollView.

A ScrollView can only contain one direct child. If you want to combine multiple views into a single view that scrolls, you need to first organize those views into another layout, like you did previously in the section called “StatusActivity Layout”, and than add that layout into ScrollView.

Typically you will want ScrollView to take all the available space on the screen. Usually, therefore you will specify its layout width and height as fill_parent.

A ScrollView is usually not manipulated from Java, so it doesn’t require an id.

In this example, we wrap our TextView with a ScrollView so that if there’s a lot of text to display, ScrollView automatically adds scroll bars.

Example 10.1. res/layout/timeline_basic.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical" android:layout_height="fill_parent"
  android:layout_width="fill_parent" android:background="@drawable/background">

  <!-- Title 1 -->
  <TextView android:layout_width="wrap_content"
    android:layout_height="wrap_content" android:layout_gravity="center"
    android:layout_margin="10dp" android:text="@string/titleTimeline"
    android:textColor="#fff" android:textSize="30sp" />

  <!-- Text output wrapper 2 -->
  <ScrollView android:layout_height="fill_parent"
    android:layout_width="fill_parent">

    <!-- Text output 3 -->
    <TextView android:layout_height="fill_parent"
      android:layout_width="fill_parent" android:id="@+id/textTimeline"
      android:background="#6000" />
  </ScrollView>

</LinearLayout>

1

This is the title that we show at the top of this Activity’s screen. Notice that we defined titleTimeline string resource in /res/values/strings.xml file, just like we did before in the section called “Strings Resource”.

2

ScrollView that is wrapping our TextView and adding scroll bars as needed.

3

TextView that shows the actual text, in this case statuses of our friends coming from the database.

Creating TimelineActivity Class

Now that we have the layout file, we need to create the TimelineActivity class. To do that, just as with any other Java file, in Eclipse Package Explorer, right-click on your com.marakana.yamba package, choose New→Class and for Name enter TimelineActivity.

And just as before, whenever we create a new Java class that is one of those main building blocks—such as Activities, Services, Broadcast Receivers, and Content Providers—we first subclass a base class provided by the Android framework. In case of Activities, that class is Activity.

The method we almost universally override in any activity is onCreate(). This is a great place for us to initialize the database. The flip side of the coin is onDestroy(), a good place to clean up anything that we create in onCreate(). In this case, we close the database in onDestroy(). Since we’d like to have the data as fresh as possible, we put the code for querying the database and outputting the data in onResume(), the method called every time this activity is brought up front. Here’s what our code looks like:

Example 10.2. TimelineActivity.java, version 1

package com.marakana.yamba5;

import android.app.Activity;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.widget.TextView;

public class TimelineActivity1 extends Activity { // 1
  DbHelper dbHelper;
  SQLiteDatabase db;
  Cursor cursor;
  TextView textTimeline;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.timeline);

    // Find your views
    textTimeline = (TextView) findViewById(R.id.textTimeline);

    // Connect to database
    dbHelper = new DbHelper(this);  // 2
    db = dbHelper.getReadableDatabase();  // 3
  }

  @Override
  public void onDestroy() {
    super.onDestroy();

    // Close the database
    db.close(); // 4
  }

  @Override
  protected void onResume() {
    super.onResume();

    // Get the data from the database
    cursor = db.query(DbHelper.TABLE, null, null, null, null, null,
        DbHelper.C_CREATED_AT + " DESC"); // 5
    startManagingCursor(cursor);  // 6

    // Iterate over all the data and print it out
    String user, text, output;
    while (cursor.moveToNext()) {  // 7
      user = cursor.getString(cursor.getColumnIndex(DbHelper.C_USER));  // 8
      text = cursor.getString(cursor.getColumnIndex(DbHelper.C_TEXT));
      output = String.format("%s: %s\n", user, text); // 9
      textTimeline.append(output); // 10
    }
  }



}

1

This is an Activity, so we start by subclassing Android framework Activity class.

2

We need access to the database in order to get the timeline data. onCreate() is a good place to connect to the database.

3

Once dbHelper opens the database file, we need to ask it for the actual database object. To do that, we can use either getReadableDatabase() or getWritableDatabase(). In this case, we are only reading the data from the timeline, so we open the database for reading only.

4

At some point we need to close the database and release that resource. If database was opened in onCreate(), the counterpart to that would be onDestroy(). So, we close the database there. Remember that onDestroy() is not called unless the system has to free up resources.

5

To query the data from the database, we use the query() method. While this method seems to contain almost endless parameters, most of them map nicely to various parts of SQL SELECT statement. So this line is equivalent to SQL SELECT * FROM timeline ORDER BY created_at DESC. The various null values refer to parts of SELECT statement we are not using, such as WHERE, GROUPING, and HAVING. The data returned to us is of type Cursor which is an iterator.

6

startManagingCursor() is a convenience method that tells the activity to start managing cursor’s lifecycle the same way it manages its own. What that means is that when this activity is about to be destroyed, it will make sure to release any data referred to by the cursor, thus helping Java’s garbage collector clean up memory faster. The alternative is for us to manually add code in various override methods and worry about cursor management ourselves.

7

cursor, if you recall from the section called “Cursors” represents all the data we got back from the database SELECT statement that was effectively executed by our query() method. This data is generally in form of a table, with many rows and columns. Each row represents a single record, such as a single status in our timeline. Each row also has columns that we predefined, such as _id, created_at, user, and txt. As we mentioned before, cursor is an iterator meaning we can step through all its data one record at a time. First call to Cursor’s moveToNext() positions the cursor at the start. moveToNext() stops when there’s no more data to process.

8

For each record that the cursor currently points to, we can ask for its value by type and column index. So cursor.getString(3) returns a String value of the status, and cursor.getLong(1) gives us the timestamp when this record was created. Refer back to Chapter 9, Database to see how we define strings such as C_USER and C_TEXT in our program that map to column names in the database. However, having hardcoded column indices is not a good practice because if we ever change the schema, we’ll have to remember to update this code. Also, the code is not very readable in this form. A better practice is to ask the database for the index of each column. We do that with the cursor.getColumnIndex() call.

9

We use String.format() to format each line of the output. In this example, as we chose TextView for our widget to display the data, we can only display text, in other words, formatted strings. In the later iteration of this code, we’ll improve on this.

10

We finally append that new line of output to our text view textTimeline so user gets to see it on the screen.

While this approach works for smaller data sets, it is not optimal or recommended. The better approach is to use a ListView to represent the list of statuses that we have in the database. ListView, which we’ll use in the next version of our TimelineActivity, is much more scalable and efficient.

About Adapters

A ScrollView will work for a few dozen records. But what if your status database has hundreds or even thousands of records? Waiting to get and print them all would be highly inefficient. The user probably doesn’t even care about all of the data anyhow.

To address this issue, Android provides adapters. These are a smart way to connect a View with some kind of data source (see Figure 10.1, “Adapter”). Typically, your view would be a ListView and the data would come in form of a Cursor or Array. So adapters come as subclasses of CursorAdapter or ArrayAdapter.

Figure 10.1. Adapter

Adapter

Adding ListView to TimelineActivity

As before, our first stop in upgrading out applications is our resources file. We’ll add a ListView to the timeline layout by editing timeline.xml.

Example 10.3. res/layout/timeline.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical" android:layout_height="fill_parent"
  android:layout_width="fill_parent" android:background="@drawable/background">
  <TextView android:layout_width="wrap_content"
    android:layout_height="wrap_content" android:layout_gravity="center"
    android:layout_margin="10dp" android:text="@string/titleTimeline"
    android:textColor="#fff" android:textSize="30sp" />

  <!-- 1 -->
  <ListView android:layout_height="fill_parent"
    android:layout_width="fill_parent" android:id="@+id/listTimeline"
    android:background="#6000" />


</LinearLayout>

1

Adding ListView to your layout is like adding any other widget. The main attributes are id, and layout_height, and layout_width.

ListView versus ListActivity

We could have also used ListActivity as the parent class for our TimelineActivity. ListActivity is an activity that has a ListView. Either approach would work, but we choose to subclass Activity and create ListView separately to provide step-by-step, incremental learning.

ListActivity is slightly easier to use in cases where there the builtin ListView is the only widget in the activity. ListActivity also makes it really easy to assign an existing array of elements to its list via the XML binding. However, since we are using a Cursor for data and not an array (because our data comes from the database), and since we do have an additional TextView for the scrollview’s title, the simplicity of ListActivity in this case is outweighed by customization we require.

Creating a Row Layout

There’s one more XML file to take care of. While timeline.xml describes the entire activity, we also need to specify what a single row of data looks like—tht is, a single line item on the screen that will show information such as who said what and when.

The easiest way to do that is to create another XML file just for that row. To do that, as for any new XML file, we use the Android New XML File dialog window: File→New→Android New XML File. Let’s name this file row.xml and select Layout for the type.

For this layout, we chose one LinearLayout going vertically and having two lines. The first line consists of the user and timestamp, and the second contains the actual status message. Notice that the first line uses another LinearLayout to position the user and timestamp horizontally next to each other.

The row of data in the ListView is represented by a custom layout defined in row.xml file.

Example 10.4. res/layout/row.xml

<?xml version="1.0" encoding="utf-8"?>
  <!-- 1 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_height="wrap_content" android:orientation="vertical"
  android:layout_width="fill_parent">

  <!-- 2 -->
  <LinearLayout android:layout_height="wrap_content"
    android:layout_width="fill_parent">

    <!-- 3 -->
    <TextView android:layout_height="wrap_content"
      android:layout_width="fill_parent" android:layout_weight="1"
      android:id="@+id/textUser" android:text="Slashdot"
      android:textStyle="bold" />

    <!-- 4 -->
    <TextView android:layout_height="wrap_content"
      android:layout_width="fill_parent" android:layout_weight="1"
      android:gravity="right" android:id="@+id/textCreatedAt"
      android:text="10 minutes ago" />
  </LinearLayout>

  <!-- 5 -->
  <TextView android:layout_height="wrap_content"
    android:layout_width="fill_parent" android:id="@+id/textText"
    android:text="Firefox comes to Android" />

</LinearLayout>

1

The main layout for the entire row. It is vertical because our row consists of two lines.

2

A layout that runs horizontally and represents the first line of data, namely the user and timestamp.

3

The user who posted this update.

4

The timestamp when it was posted. It should be relative time (e.g. 10 minutes ago).

5

The actual status.

Creating an Adapter in TimelineActivity.java

Now that we have the XML files sorted out, we are ready to update the Java code. First we need to create the adapter. Adapters generally come in two flavors: those that represent array data and those that represent cursor data. Since our data is coming from the database, we are going to use the cursor-based adapter. One of the simplest of those is SimpleCursorAdapter.

SimpleCursorAdapter requires us to describe a single row of data (which we do in row.xml), the data (a cursor in our case) and the mapping for a single record of data to the single row in the list. The last parameter maps each cursor column to a view in the list.

Example 10.5. TimelineActivity.java, version 2

package com.marakana.yamba5;

import android.app.Activity;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;

public class TimelineActivity2 extends Activity {
  DbHelper dbHelper;
  SQLiteDatabase db;
  Cursor cursor;  // 1
  ListView listTimeline;  // 2
  SimpleCursorAdapter adapter;  // 3
  static final String[] FROM = { DbHelper.C_CREATED_AT, DbHelper.C_USER,
      DbHelper.C_TEXT };  // 4
  static final int[] TO = { R.id.textCreatedAt, R.id.textUser, R.id.textText }; // 5

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.timeline);

    // Find your views
    listTimeline = (ListView) findViewById(R.id.listTimeline);  // 6

    // Connect to database
    dbHelper = new DbHelper(this);
    db = dbHelper.getReadableDatabase();
  }

  @Override
  public void onDestroy() {
    super.onDestroy();

    // Close the database
    db.close();
  }

  @Override
  protected void onResume() {
    super.onResume();

    // Get the data from the database
    cursor = db.query(DbHelper.TABLE, null, null, null, null, null,
        DbHelper.C_CREATED_AT + " DESC");
    startManagingCursor(cursor);

    // Setup the adapter
    adapter = new SimpleCursorAdapter(this, R.layout.row, cursor, FROM, TO);  // 7
    listTimeline.setAdapter(adapter); // 8
  }

}

1

Cursor to all the status updates that we have in the database.

2

listTimeline is our ListView that displays the data.

3

adapter is our custom adapter, explained in the text that follows this example.

4

FROM is a String array specifying which columns in the cursor we’re binding from. We use the same strings that we’ve already used to refer to columns in our program

5

TO is an array of integers representing IDs of Views in the row.xml layout that we are binding data to. The number of elements in FROM and TO must be the same, so that element at index 0 in FROM maps to element 0 in TO, and so on.

6

We get the ListView from the XML layout.

7

Once we have the data as a cursor, the layout of a single row from the row.xml file, and the FROM and TO constants for mapping the data, we are ready to create the SimpleCursorAdapter.

8

Finally, we need to tell our ListView to use this adapter.

At this point, TimelineActivity is complete, but not yet registered with the Manifest file. We’ll do that in the next section. However, if we were to run this activity, you’d quickly notice that the timestamp doesn’t quite look the way we imagined it.

Remember that we are storing the time of status creation in the database as a long value representing the number of milliseconds since January 1st, 1970. And since that’s the value in the database, that’s the value we show on the screen as well. This is the standard Unix Time and as such is very useful for representing actual points in time. But the value is not very meaningful to users. Instead of showing value 1287603266359, it would be much nicer to represent it to the user as "10 Minutes Ago." This friendly time format is known as relative time and Android provides a method to convert from one format to the other.

The question is where to inject this conversion. As it stands right now, the SimpleCursorAdapter is capable only of mapping straight from a database value to layout view. This doesn’t work for our needs, because we need to add some business logic in between the data and the view. To do this, we’ll create our own adapter.

TimelineAdapter

TimelineAdapter is our custom adapter. Although SimpleCursorAdapter did a straightforward mapping of data in the database to views on the screen, we had an issue with the timestamp. The job of TimelineAdapter is to inject some business logic to convert Unix timestamp to relative time. We can discover that the method in SimpleCursorAdapter`that creates a displayable view from input data is `bindView(), so we’ll override that method and ask it to massage the data before it is displayed.

Typically, if you are not sure what method to override, look at the online documentation for that particular system class that you are modifying. In this case, that’d be http://developer.android.com/reference/android/widget/SimpleCursorAdapter.html.

Example 10.6. TimelineAdapter.java

package com.marakana.yamba5;

import android.content.Context;
import android.database.Cursor;
import android.text.format.DateUtils;
import android.view.View;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;

public class TimelineAdapter extends SimpleCursorAdapter { // 1
  static final String[] FROM = { DbHelper.C_CREATED_AT, DbHelper.C_USER,
      DbHelper.C_TEXT }; // 2
  static final int[] TO = { R.id.textCreatedAt, R.id.textUser, R.id.textText }; // 3

  // Constructor
  public TimelineAdapter(Context context, Cursor c) { // 4
    super(context, R.layout.row, c, FROM, TO);
  }

  // This is where the actual binding of a cursor to view happens
  @Override
  public void bindView(View row, Context context, Cursor cursor) { // 5
    super.bindView(row, context, cursor);

    // Manually bind created at timestamp to its view
    long timestamp = cursor.getLong(cursor
        .getColumnIndex(DbHelper.C_CREATED_AT)); // 6
    TextView textCreatedAt = (TextView) row.findViewById(R.id.textCreatedAt); // 7
    textCreatedAt.setText(DateUtils.getRelativeTimeSpanString(timestamp)); // 8
  }

}

1

To create our own custom adapter, we subclass one of the Android standard adapters, in this case the same SimpleCursorAdapter we used in the previous section.

2

This constant defines the columns of interest to us in the database, as in the previous example.

3

This constant specifies the IDs of views that we’ll map those columns to.

4

Because we’re defining a new class, we need a constructor. It simply calls the parent constructor using super.

5

The only method we override is bindView(). This method is called for each row to map its data to its views, and it’s where the gist of adapter work happens. In order to reuse most of the data-to-views mapping provided by SimpleCursorAdapter, we call super.bindView() first.

6

To override default mapping for timestamp, we first get the actual timestamp value from the database.

7

Next, we find the specific TextView in the row.xml file.

8

Finally, we set the value of textCreatedAt to the relative time since the timestamp. To do this, we use Android SDK method DateUtils.getRelativeTimeSpanString().

At this point, we can further simplify our TimelineActivity class, because we moved some of the adapter details to TimelineAdapter.

Example 10.7. TimelineActivity.java, version 3

package com.marakana.yamba5;

import android.app.Activity;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.widget.ListView;

public class TimelineActivity3 extends Activity {
  DbHelper dbHelper;
  SQLiteDatabase db;
  Cursor cursor;
  ListView listTimeline;
  TimelineAdapter adapter;  // 1

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.timeline);

    // Find your views
    listTimeline = (ListView) findViewById(R.id.listTimeline);

    // Connect to database
    dbHelper = new DbHelper(this);
    db = dbHelper.getReadableDatabase();
  }

  @Override
  public void onDestroy() {
    super.onDestroy();

    // Close the database
    db.close();
  }

  @Override
  protected void onResume() {
    super.onResume();

    // Get the data from the database
    cursor = db.query(DbHelper.TABLE, null, null, null, null, null,
        DbHelper.C_CREATED_AT + " DESC");
    startManagingCursor(cursor);

    // Create the adapter
    adapter = new TimelineAdapter(this, cursor);  // 2
    listTimeline.setAdapter(adapter); // 3
  }

}

1

We change SimpleCursorAdapter to TimelineAdapter.

2

Create a new instance of the TimelineAdapter and pass it the context and the data.

3

Set our ListView to connect to the data via the adapter.

One of the shortcomings of overriding bindView() is that we use super.bindView() to bind all views first, then replace its behavior for one particular element. This is somewhat wasteful. The final version of our application in this chapter will optimize the process.

ViewBinder: A Better Alternative to TimelineAdapter

Instead of creating a new TimelineAdapter that is a subclass of SimpleCursorAdapter and overriding its bindView() method, we could also attach the business logic directly to the existing SimpleCursorAdapter. This approach is more efficient because we are not replacing what bindView() already does and we do not require a separate custom adapter class.

To attach business logic to an existing SimpleCursorAdapter, use its setViewBinder() method. We will need to supply the method with an implementation of ViewBinder. ViewBinder is an interface that specifies setViewValue(), where the actual binding of a particular date element to particular view happens.

Again, we discovered setViewBinder() feature of this SimpleCursorAdapter framework class by reading reference documentation for it.

In our final iteration of TimelineAdapter, we create a custom ViewBinder as a constant and attach it to the stock SimpleCursorAdapter.

Example 10.8. TimelineActivity.java with ViewBinder

  ...

  @Override
  protected void onResume() {
    ...
    adapter.setViewBinder(VIEW_BINDER); // 1
    ...
  }

  // View binder constant to inject business logic that converts a timestamp to
  // relative time
  static final ViewBinder VIEW_BINDER = new ViewBinder() { // 2

    public boolean setViewValue(View view, Cursor cursor, int columnIndex) { // 3
      if (view.getId() != R.id.textCreatedAt)
        return false; // 4

      // Update the created at text to relative time
      long timestamp = cursor.getLong(columnIndex); // 5
      CharSequence relTime = DateUtils.getRelativeTimeSpanString(view
          .getContext(), timestamp); // 6
      ((TextView) view).setText(relTime); // 7

      return true; // 8
    }

  };

  ...

1

We attach a custom ViewBinder instance to our stock adapter. VIEW_BINDER is defined further down in our code.

2

The actual implementation of a ViewBinder instance. Notice that we are implementing it as an inner class. That’s because there’s no reason for any other class to use it and thus shouldn’t be exposed to the outside world. Also notice that it is static final, meaning that it’s a constant.

3

The only method that we need to provide is setViewValue(). This method is called for each data element that needs to be bound to a particular view.

4

First we check whether this view is the view we care about, i.e., our TextView representing when the status was created. If not, we return false, which causes the adapter to handle the bind itself in the standard manner. If it is our view, we move on and do the custom bind.

5

We get the raw timestamp value from the cursor data.

6

Using the same Android helper method we used in our previous example, DateUtils.getRelativeTimeSpanString(), we convert the timestamp to the human-readable format. This is that business logic that we are injecting.

7

Update the text on the actual view.

8

Return true so that SimpleCursorAdapter does not process bindView() on this element in its standard way.

Updating Manifest File

Now that we have the TimelineActivity, it would make sense to make it "the main" activity for Yamba application. After all, users are more likely to want to check what their friends are doing than to update their own status.

To do that, we need to update the manifest file. As usual, we’ll list TimelineActivity within the <activity> element in the AndroidManifest.xml file, just as we added preference activity to the manifest file in the section called “Update Manifest File”:

<activity android:name=".TimelineActivity" />

Now, in order to make TimelineActivity the main entry point into our application, we need to register it to respond to certain intents. Basically, when the user clicks to start your application, the system sends an intent. You have to define an activity to "listen" to this intent. The activity does that by filtering the intents using an IntentFilter. In XML, this is within the <intent-filter> element and it usually contains at least an <action> element representing the actual intent action we’re interested in.

You may have noticed that StatusActivity had some extra XML compared to PrefsActivity. The extra code is the intent filter block, along with the action that it’s filtering for.

There is a special action named android.intent.action.MAIN that simply indicates that this is the main component that should be started when the user wants to start your application. Additionally, there’s a <category> element that tells the system that this application should be added to the main Launcher application so that the user can see the app icon along with all the other icons, click on it, and start it. This category is defined as android.intent.category.LAUNCHER.

So, to make TimelineActivity the main entry point, we simply list it and move the code from the StatusActivity declaration over to the TimelineActivity declaration.

Example 10.9. AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  android:versionCode="1" android:versionName="1.0" package="com.marakana.yamba5">
  <application android:icon="@drawable/icon" android:label="@string/app_name"
    android:name=".YambaApplication">

    <activity android:name=".TimelineActivity" android:label="@string/titleTimeline">
      <intent-filter> <!-- 1 -->
        <action android:name="android.intent.action.MAIN" /> <!-- 2 -->
        <category android:name="android.intent.category.LAUNCHER" /> <!-- 3 -->
      </intent-filter>
    </activity>

    <activity android:name=".PrefsActivity" android:label="@string/titlePrefs" />
    <activity android:name=".StatusActivity" android:label="@string/titleStatus" /> <!-- 4 -->

    <service android:name=".UpdaterService" />

  </application>
  <uses-sdk android:minSdkVersion="8" />

  <uses-permission android:name="android.permission.INTERNET" />
</manifest>

1

<intent_filter> registers this particular activity with the system to respond to certain intents.

2

Tells the system that this is the main activity to start when users chooses to start your application.

3

The category LAUNCHER tells the Home application to add this application into the list of applications it displays in the launcher drawer.

4

StatusActivity no longer needs any intent filters.

Initial App Setup

Now, when the user runs our application, the Timeline screen will show up first. But unless the user knows she should set up the preferences and start the service, there will be no data and very little hand holding telling her what to do.

One solution to that would be to check whether preferences exist, and if they do not, redirect the user to the Preference activity with a message what to do next.

...
@Override
protected void onCreate(Bundle savedInstanceState) {
  ...
  // Check whether preferences have been set
  if (yamba.getPrefs().getString("username", null) == null) { // 1
    startActivity(new Intent(this, PrefsActivity.class)); // 2
    Toast.makeText(this, R.string.msgSetupPrefs, Toast.LENGTH_LONG).show(); // 3
  }
  ...
}
...

1

We check whether one of the preferences have been set. In this case, I’ve shosen to check username because it’s likely to be set if any preferences at all are set. Since the first time user runs the application, the preferences do not exist, this means the value of username (or any other preference item we choose) will be null.

2

We start the PrefsActivity. Note that startActivity() will dispatch an intent to the system, but the rest of onCreate() will execute as well. This is good since we’re likely going to come back to the Timeline activity once we’re done setting up preferences.

3

We display a little pop-up message, i.e. a Toast telling the user what to do. This assumes that you have created the appropriate msgSetupPrefs in your strings.xml file, as usual.

Base Activity

Now that we have a Timeline activity, we need to give it an options menu, just as we did to our Status activity in the section called “Options Menu”. This is especially important because the Timeline activity is the entry point into our application and without the menu, the user cannot easily get to any other activity or start and stop the service.

As one approach, we could copy and paste the code we already have from the Status activity, but that’s rarely a good strategy. Instead, we’ll do what we usually do: refactor the code. In this case, we can take out the common functionality from the Status activity and place it in another activity that will serve as the base. See Figure 10.2, “BaseActivity Refactor”.

Figure 10.2. BaseActivity Refactor

BaseActivity Refactor

To do that, we’ll create a new class called BaseActivity and move the the common functionality into it. For us, the common functionality includes getting the reference to the YambaApplication object, as well as the onCreateOptionsMenu() and onOptionsItemSelected() methods that support the options menu.

Toggle Service

While we’re at it, instead of having Start Service and Stop Service menu buttons, it would be nice to provide just one button that toggles between Start and Stop. To do that, we’ll change our menu and add onMenuOpened() to the base activity to dynamically update the title and images for this toggle item.

First, we’ll update the menu.xml file to include our new toggle menu item. At the same time, we’ll remove the Start Service and Stop Service items because our toggle feature makes them obsolete.

Example 10.10. res/menu/menu.xml[]

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

  <item android:id="@+id/itemStatus" android:title="@string/titleStatus"
    android:icon="@android:drawable/ic_menu_edit"></item>
  <item android:title="@string/titleTimeline" android:id="@+id/itemTimeline"
    android:icon="@android:drawable/ic_menu_sort_by_size"></item>
  <item android:id="@+id/itemPrefs" android:title="@string/titlePrefs"
    android:icon="@android:drawable/ic_menu_preferences"></item>
  <item android:icon="@android:drawable/ic_menu_delete"
    android:title="@string/titlePurge" android:id="@+id/itemPurge"></item>

  <!-- 1 -->
  <item android:id="@+id/itemToggleService" android:title="@string/titleServiceStart"
    android:icon="@android:drawable/ic_media_play"></item>

</menu>

1

This new itemToggleService now replaces both itemServiceStart and itemServiceStop.

Next, we need to override onMenuOpened() in the base activity to change the menu item dynamically.

Example 10.11. BaseActivity.java

package com.marakana.yamba5;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

/**
 * The base activity with common features shared by TimelineActivity and
 * StatusActivity
 */
public class BaseActivity extends Activity { // 1
  YambaApplication yamba; // 2

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    yamba = (YambaApplication) getApplication(); // 3
  }

  // Called only once first time menu is clicked on
  @Override
  public boolean onCreateOptionsMenu(Menu menu) { // 4
    getMenuInflater().inflate(R.menu.menu, menu);
    return true;
  }

  // Called every time user clicks on a menu item
  @Override
  public boolean onOptionsItemSelected(MenuItem item) { // 5

    switch (item.getItemId()) {
    case R.id.itemPrefs:
      startActivity(new Intent(this, PrefsActivity.class)
          .addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
      break;
    case R.id.itemToggleService:
      if (yamba.isServiceRunning()) {
        stopService(new Intent(this, UpdaterService.class));
      } else {
        startService(new Intent(this, UpdaterService.class));
      }
      break;
    case R.id.itemPurge:
      ((YambaApplication) getApplication()).getStatusData().delete();
      Toast.makeText(this, R.string.msgAllDataPurged, Toast.LENGTH_LONG).show();
      break;
    case R.id.itemTimeline:
      startActivity(new Intent(this, TimelineActivity.class).addFlags(
          Intent.FLAG_ACTIVITY_SINGLE_TOP).addFlags(
          Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
      break;
    case R.id.itemStatus:
      startActivity(new Intent(this, StatusActivity.class)
          .addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
      break;
    }
    return true;
  }

  // Called every time menu is opened
  @Override
  public boolean onMenuOpened(int featureId, Menu menu) { // 6
    MenuItem toggleItem = menu.findItem(R.id.itemToggleService); // 7
    if (yamba.isServiceRunning()) { // 8
      toggleItem.setTitle(R.string.titleServiceStop);
      toggleItem.setIcon(android.R.drawable.ic_media_pause);
    } else { // 9
      toggleItem.setTitle(R.string.titleServiceStart);
      toggleItem.setIcon(android.R.drawable.ic_media_play);
    }
    return true;
  }

}

1

BaseActivity is an Activity.

2

We declare the shared YambaApplication to make it accessible to all the other subclasses.

3

In onCreate(), we get the reference to this yamba.

4

onCreateOptionsMenu() is moved here from StatusActivity.

5

onOptionsItemSelected() is also moved over from StatusActivity. Notice, however, that it now checks for itemToggleService instead of start and stop service items. Based on state of the service, which we know from the flag in yamba, we request either to start or to stop the Updater service.

6

onMenuOpened() is the new method called by the system when the options menu is opened. This is a good callback for us to implement the toggle functionality. We’re given the menu object that represents the options menu.

7

Within the menu object, we find our new toggle item so that we can update it based on the current state of the Updater service.

8

We check whether the service is already running, and if it is, we set the appropriate title and icon for the toggle item. Notice that here we’re setting up the title and icon programmatically using the Java APIs, instead of the XML which we used initially to set up the menu in menu.xml.

9

If the service is stopped, we set the icon and title so that user can click on it and start the service. This way our single toggle button communicates whatever state the service is currently in.

Now that we have a BaseActivity class, let’s update our Timeline activity to use it. Here’s what the completed Timeline activity looks like:

Example 10.12. TimelineActivity.java, final version

package com.marakana.yamba5;

import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.text.format.DateUtils;
import android.view.View;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.SimpleCursorAdapter.ViewBinder;

public class TimelineActivity extends BaseActivity { // 1
  Cursor cursor;
  ListView listTimeline;
  SimpleCursorAdapter adapter;
  static final String[] FROM = { DbHelper.C_CREATED_AT, DbHelper.C_USER,
      DbHelper.C_TEXT };
  static final int[] TO = { R.id.textCreatedAt, R.id.textUser, R.id.textText };

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.timeline);

    // Check if preferences have been set
    if (yamba.getPrefs().getString("username", null) == null) { // 2
      startActivity(new Intent(this, PrefsActivity.class));
      Toast.makeText(this, R.string.msgSetupPrefs, Toast.LENGTH_LONG).show();
    }

    // Find your views
    listTimeline = (ListView) findViewById(R.id.listTimeline);
  }

  @Override
  protected void onResume() {
    super.onResume();

    // Setup List
    this.setupList(); // 3
  }

  @Override
  public void onDestroy() {
    super.onDestroy();

    // Close the database
    yamba.getStatusData().close(); // 4
  }

  // Responsible for fetching data and setting up the list and the adapter
  private void setupList() { // 5
    // Get the data
    cursor = yamba.getStatusData().getStatusUpdates();
    startManagingCursor(cursor);

    // Setup Adapter
    adapter = new SimpleCursorAdapter(this, R.layout.row, cursor, FROM, TO);
    adapter.setViewBinder(VIEW_BINDER); // 6
    listTimeline.setAdapter(adapter);
  }

  // View binder constant to inject business logic for timestamp to relative
  // time conversion
  static final ViewBinder VIEW_BINDER = new ViewBinder() { // 7

    public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
      if (view.getId() != R.id.textCreatedAt)
        return false;

      // Update the created at text to relative time
      long timestamp = cursor.getLong(columnIndex);
      CharSequence relTime = DateUtils.getRelativeTimeSpanString(view
          .getContext(), timestamp);
      ((TextView) view).setText(relTime);

      return true;
    }

  };
}

1

For starters, we now subclass our BaseActivity instead of just of the system’s Activity. This way we inherit the yamba object as well as all the support for the options menu.

2

This is where we check whether preferences are already set. If not, we’ll redirect the user to the Preference activity first.

3

On resuming this activity, we set up the list. This is a private method, shown further down.

4

When this activity is closed, we want to make sure we close the database to release this resource. The database is opened by the call to getStatusUpdates() in the yamba application.

5

setupList() is the convenience method that gets the data, sets up the adapter, and connects it all to the list view.

6

This is where we attach the view binder to the list, as discussed already in the section called “ViewBinder: A Better Alternative to TimelineAdapter”.

7

ViewBinder is defined here.

At this point, we’ve done a lot of the refactoring work on our Timeline activity. We can also simplify the Status activity by cutting out the code related to the options menu. This makes it simpler and helps separate functional concerns among BaseActivity, StatusDate, and TimelineActivity.

Figure 10.3, “TimelineActivity” shows what the final Timeline activity screen looks like.

Figure 10.3. TimelineActivity

TimelineActivity

Summary

At this point, Yamba can post a new status as well as list statuses of our friends. Our application is complete and usable.

Figure 10.4, “Yamba Completion” illustrates what we have done so far as part of the design outlined in Figure 5.4, “Yamba Design Diagram”.

Figure 10.4. Yamba Completion

Yamba Completion

Site last updated on: April 8, 2011 at 12:51:47 PM PDT
Cover for Learning Android

View 2 comments

  1. BillS – Posted Nov. 3, 2010

    1 below could use re-wording.

  2. Marilyn Escue – Posted Nov. 29, 2010

    Yes, please reword the second sentence within bullet #1.

    Overall, going through these 3 steps seem like a good approach for the readers.

Add a comment

View 2 comments

  1. BillS – Posted Nov. 3, 2010

    yo = you

  2. Frank Maker – Posted Jan. 3, 2011

    s/tht scrools/that scrolls/

Add a comment

View 1 comment

  1. Anna Teittinen – Posted Jan. 10, 2011

    Should mention that the variable 'titleTimeline' needs to be added to strings.xml file.

Add a comment

View 3 comments

  1. Frank Maker – Posted Jan. 3, 2011

    Note: Remember that onDestroy() is not called unless the system has to free up resources.

  2. redcurry – Posted June 12, 2011

    The layout was named timeline_basic, but in the following code it's just named timeline.

  3. Kimberley Coburn – Posted July 23, 2011

    When we left off in Chapter 9, we had refactored DbHelper as an inner class of StatusData and we had delegated all DB activities (creation, access, db.close() etc.) to StatusData. In keeping with that, this is what my first version of TimeLineActivity looks like:

    public class TimeLineActivity extends Activity { YambaApplication yamba; TextView textTimeLine; Cursor cursor;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.timeline_basic);
        yamba = (YambaApplication)getApplication();
        textTimeLine = (TextView) findViewById(R.id.textTimeline);
    }
    
    @Override
    protected void onResume() {
        super.onResume();
    
        cursor = yamba.getStatusData().getStatusUpdates();
        startManagingCursor(cursor);
    
        String user, text, output;
    
        while (cursor.moveToNext()) {
            user = cursor.getString(cursor.getColumnIndex(StatusData.C_USER));
            text = cursor.getString(cursor.getColumnIndex(StatusData.C_TEXT));
            output = String.format("%s: %s\n", user, text);
            textTimeLine.append(output);
        }
    
    }
    

    }

Add a comment

View 4 comments

  1. BillS – Posted Nov. 3, 2010

    I seem to remember a presentation at this past year's Google I/O about the dangers of using ListView. Might want to mention some of those pitfalls here or suggest an alternative method to ListView.

  2. Marilyn Escue – Posted Nov. 29, 2010

    Overall, nice explanations of what is going on in the bullet points above.

    Just one question about cursor.MoveToNext... is there any chance that the first row is skipped when this is called? Or, when cursor is first initialized, it technically is "before" the first row and then "moves" to the first row on the first iteration? Might want to explain that one piece either way.

  3. Nigel Gilbert – Posted Feb. 21, 2011

    At this point, I was expecting to be able to run the application and see the status update timeline even though it is inefficient and simple.

    Without having a functional application the later improvements/refactorings seem a bit theoretical since the reader has to examine some code/config which will never run since it will be replaced soon by improvements, this just seems a little odd to me.

    I suggest moving the "Updating Manifest File" and "BaseActivity" sections up to this point and then proceeding with the improvements from a more solid base.

  4. Kimberley Coburn – Posted July 23, 2011

    I agree with Nigel. But, it's an easy fix: Add the TimeLineActivity to the manifest and a new option to the existing menu. Not perfect, since you always have to return the StatusActivity to use the menu, but OK for a quick test.

Add a comment

View 1 comment

  1. Paul Steiner – Posted Oct. 5, 2011

    'in upgrading out applications' should read 'in upgrading our application'

Add a comment

View 1 comment

  1. Paul Steiner – Posted Oct. 5, 2011

    'tht is' should be 'that is'

Add a comment

View 1 comment

  1. Frank Maker – Posted Jan. 3, 2011

    database cursor data

Add a comment

View 3 comments

  1. Anna Teittinen – Posted Jan. 10, 2011

    The TimelineActivity does not yet compile at this point.

    References to DbHelper.TABLE, DbHelper.C_CREATED_AT, DbHelper.C_USER, DbHelper.C_TEXT have been refactored to the StatusData class from section Chapter 9, section "Refactoring Status Data".

    The code in TimelineActivity needs to reflect this.

  2. Anna Teittinen – Posted Jan. 10, 2011

    Compiling error resulted in the TimelineActivity2 code above on line: dbHelper = new DbHelper(this);

    The compiling error is: "No enclosing instance of type StatusData is accessible. Must qualify the allocation with an enclosing instance of type StatusData (e.g. x.new A() where x is an instance of StatusData)."

    DbHelper is an inner class of Status Data. Thus, the fix to the compiling error would be: dbHelper = ((YambaApplication)getApplication()).getStatusData().new DbHelper(this);

  3. Anna Teittinen – Posted Jan. 10, 2011

    The line above indicating that "TimelineActivity is complete and you can run your application" is misleading since this class has not yet been added to the AndroidManifest.xml file.

Add a comment

View 1 comment

  1. BillS – Posted Nov. 3, 2010

    crate = create

Add a comment

View 2 comments

  1. BillS – Posted Nov. 3, 2010

    January 1st, 1970

  2. Anna Teittinen – Posted Jan. 10, 2011

    References to DbHelper.C_CREATED_AT, DbHelper.C_USER, DbHelper.C_TEXT have been refactored to the StatusData class from section Chapter 9, section "Refactoring Status Data".

    The code in TimelineAdapter below needs to reflect this.

Add a comment

View 1 comment

  1. Marilyn Escue – Posted Nov. 29, 2010

    In bullet #5 above, "jest" should be "gist".

Add a comment

View 1 comment

  1. redcurry – Posted June 13, 2011

    If ViewBinder is a better alternative to TimelineAdapter, why even include the TimelineAdapter section? I don't think we've learned much from it.

Add a comment

View 1 comment

  1. Tudor Luca – Posted Feb. 14, 2012

    I have a very weird problem with the app: it doesn't show the correct time passed since the Status has been posted. And here is the weird part: on the emulator it works perfectly, it shows the correct time elapsed (eg: 10 minutes ago), but on an actual smartphone (I'm using a Samsung Galaxy S2 v2.3.3) it shows the time elapsed since the Status has been inserted into the database. Any ideas why?

Add a comment

View 2 comments

  1. Marilyn Escue – Posted Nov. 29, 2010

    In bullet #3 below, is there some advantage to creating an inner class? Can a short explanation be added here for the reasoning?

  2. Nigel Gilbert – Posted Feb. 23, 2011

    When resolving the ViewBinder class, it wasn't completely obvious that it should be:

    import android.widget.SimpleCursorAdapter.ViewBinder;
    

    I had a choice of 3 possible resolutions and whilst the answer was fairly easy to find in the SimpleCursorAdapter documentation, I think it might help to be explicit here.

Add a comment

Add a comment

    Add a comment

    View 1 comment

    1. Nigel Gilbert – Posted Feb. 23, 2011

      The minSdkVersion parameter should be same as the one used when the project was set up in Chapter 6.

    Add a comment

    View 1 comment

    1. Anna Teittinen – Posted Jan. 10, 2011

      Need to mention that: 1. the modifications below are for the TimelineActivity class. 2. YambaApplication class needs a 'getPrefs()' method.

    Add a comment

    View 1 comment

    1. Nigel Gilbert – Posted Feb. 23, 2011

      It might be good to explain why the activities are launched with the FLAG_ACTIVITY_REORDER_TO_FRONT flag.

    Add a comment

    View 2 comments

    1. Anna Teittinen – Posted Jan. 11, 2011
      1. Should explain more clearly why the following members existed in TimelineActivity3 but are removed (also their usages) from this final version of the TimelineActivity.

      DbHelper dbHelper, SQLiteDatabase db, and TimelineAdapter adapter

      1. In method onOptionsItemSelected(), the call to delete() in ((YambaApplication) getApplication()).getStatusData().delete(); does not exist. Compilation error occurs.

      Need to explain what delete() should do and add that method to the StatusData class.

      For now, I am adding an empty delete() method to StatusData just so that the code can compile.

    2. Nigel Gilbert – Posted Feb. 23, 2011

      s/StatusDate/StatusData/

    Add a comment