Chapter 5. Adapters, List Views, and Lists

Lists, in Android, are one of the most often-used tools to show data to users. From the entry menu of a game to a dynamic list of Facebook statuses or Twitter messages, lists are everywhere. Android’s system for dealing with them, while complicated at first, becomes much easier once you begin using it. In this chapter, I’ll run the gamut from simple, static main-menu lists to the dynamic, remote-data-backed custom list elements of a Reddit feed (the social news and entertainment website). Along the way, I’ll expose you to the inner workings of one of Android’s most often-used and complex UI views.

Two Pieces to Each List

To display lists of ordered data with Android, there are two major components you’ll need to deal with.

ListView

First, you’ll need a ListView in which to display your content. This is the view whose job it is to display the information. It can be added to any screen layout, or you can use Android’s ListActivity or ListFragment to handle some of the organization for you. If your screen is primarily designed to show a collection of data to the user in list form, I highly suggest you use ListActivity and its cousin ListFragment.

Adapter

The second major class you’ll need to deal with is the Adapter. This is the object that will feed the individual views, a little bit at a time, to the ListView. It’s also responsible for filling and configuring the individual rows to be populated in the ListView. There are as many Adapter subclasses as drops of water in the ocean (all right, perhaps slightly fewer), and they cover the range of data types—from static string lists (ArrayAdapters) to the more dynamic lists (CursorAdapters). You can extend your own adapter (which I’ll show you in the second half of this chapter). For now, let me show you how to create a simple main menu with a ListView.

As always, you can either follow along with the sample code I’ve posted at Peachpit.com/androiddevelopanddesign or open your IDE and do the tasks I’ve outlined.

A Main Menu

Main menus can take any number of forms. From games to music apps, they provide a top-level navigation for the app as a whole.

They are also, as a happy side effect, a great way to introduce you to how lists work. I’ll be creating an array of strings for the resource manager, feeding it to an array adapter, and plugging that array adapter into the list view contained by a list activity. Got all that? There are a lot of moving parts to collect when dealing with lists, so I’ll take it slowly and step by step.

Creating the Menu Data

A menu must have something to display, so you need to create a list of strings to be displayed. Remember the chapter where you learned that all displayed string constants should go into the res/values/strings.xml file? String arrays, coincidentally, go into the same file, but with a slightly different syntax. I’ve added the following to my res/values/strings.xml file:

<?xml version="1.0" encoding="utf-8"?>
<resources>
   <!--The rest of the app's strings here-->
   <string name="app_name">List Example</string>
   <string name="main_menu">Main Menu</string>
   <string-array name="menu_entries">
     <item>Menu Item One</item>
     <item>Menu Item Two</item>
     <item>Menu Item Three</item>
     <item>Menu Item Four</item>
     <item>Menu Item Five</item>
     <item>Menu Item Six</item>
     <item>Menu Item Seven</item>
   </string-array>

</resources>

Instead of defining each constant inside a string tag, this time you’ll declare a string array with a name, and then each element within it can be defined inside an item tag. Now that you have data, it’s time to create an activity in which to house it.

Creating a ListActivity

Now you need a place to display your items. You’ll create an instance of ListActivity in which to display your recently created list.

Every screen must have an activity, and list screens are no exception. In this case, Android provides you a helper class that was built specifically to make list screens easier. It’s called the ListActivity, and it behaves exactly like an activity does except that it has a few extra methods to make life easier. If you’re coding along with the chapter, you’ll need to create a new project. Take the main activity you’d normally have, and modify it to look like the following listing:

package com.peachpit.lists;
import android.app.ListActivity;
import android.os.Bundle;
public class MainMenuActivity extends ListActivity{
   public void onCreate(Bundle bundle){
      super.onCreate(bundle);
      setContentView(R.layout.list_activity);
   }
}

This code will not, however, compile at the moment, because I haven’t yet defined what R.layout.list_activity looks like. Guess what you’re going to do next?

Defining a Layout for Your ListActivity

You will need to create an XML layout file for your list. Again, this is similar to other layout tasks you’ve done, with one notable exception: You need to define a ListView with the special ID android:id/list. This is what tells the system which list view is the main list view your new ListActivity will interact with. I’ve also added a TextView to the layout as a large title. My XML file looks like the following:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical">
   <TextView
     android:layout_width="match_parent"
     android:layout_height="0dp"
     android:layout_weight="1"
     android:gravity="center"
     android:text="@string/main_menu"
     android:gravity="center"
     android:textSize="40sp" />
   <ListView
     android:id="@android:id/list"
     android:layout_width="match_parent"
     android:layout_height="0dp"
     android:layout_weight="1"
     android:gravity="center" />
</LinearLayout>


Image Tip

Special IDs: You only need to call the android:id/list ListView if you’re using the built-in convenience methods of ListActivity. If you’re using a regular activity, you can use any ID you want. This special ID is what connects the ListActivity to the single ListView with which it is going to interact.


This XML layout code should look familiar to you, given what you’ve read in previous chapters. It’s simply splitting the screen space between the title main menu and the list of sub-screens. You can also see the special Android list ID that is needed to tell the ListActivity which view it should interact with.

Making a Menu List Item

Now you’ll create a layout XML file for the individual list element.

You’ll need to declare a separate layout object to define how each element will look in the list. I’m using a very simple version of the ArrayAdapter, so at this point, the layout XML file must contain only a single text view. We’ll get into more-complex menu items later in the chapter.

Next, you’ll need to create a new file, containing a single text view, in the /res/layout/ folder. Here’s what /res/layout/list_element.xml looks like in my project:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:gravity="center"
   android:layout_height="wrap_content"
   android:gravity="center"
   android:textSize="20sp"
   android:padding="15dp" />

You don’t actually need to supply an ID for this text view, because you’ll be referencing it in its capacity as a layout object (R.layout.list_element, in this case). Setting the gravity to center tells the view that you want the text to lie in the center of the extra available space. Setting the padding to 15dp will also give the views a little bit of extra space, so people with hands like mine can hit the correct one.

Now that I’ve declared what I want the list elements to look like, I can go about adding them to the ListView itself.

Creating and Populating the ArrayAdapter

Create and configure an ArrayAdapter. The ArrayAdapter will communicate your data to the ListView. It will also inflate however many copies of the list_element layout are needed to keep the ListView full of data. As a last step, here’s what you’ll need to add to the MainMenuActivity’s onCreate method:

public void onCreate(Bundle bundle){
   super.onCreate(bundle);
   setContentView(R.layout.list_activity);
   ArrayAdapter<String> adapter = ArrayAdapter.createFromResource(this,
   R.array.menu_entries, R.layout.list_element);
   setListAdapter(adapter);
}

Because the ListView has the special @android:id/list system ID, the ListActivity knows where to find the ListView. As a result, you’ll only have to create the adapter and hand it over to the ListActivity. The ListActivity will make sure that it’s correctly plugged into the ListView and that everything is drawn correctly.

To create the ArrayAdapter, I specify the array of strings I defined in the section “Creating the Menu Data” as well as the list_element layout I created in “Making a Menu List Item.” Assuming that all your components are hooked up correctly, the resulting screen will look something like Figure 5.1.

Image

Figure 5.1 A very basic main menu

Do a little dance—you’ve now got a functional (albeit very simple) list! Have a cup of coffee, sip of wine, or dog treat. Whatever you do to reward yourself for a job well done, do it now. I’ll be here when you get back.

Reacting to Click Events

Your code will need to listen for item clicks.

What’s the point of having a menu if you can’t tell when items have been selected? Right, there isn’t one. Let me show you the final piece to my basic list menu example. Add the following method to your MainMenuActivity.java file:

@Override
public void onListItemClick(ListView listView, View clickedView,
        int position, long id) {
   super.onListItemClick(listView clickedView, position, id);
   TextView tv = (TextView) clickedView;
   String clickText = "List Item " + tv.getText() + " was clicked!";
   Toast.makeText(getBaseContext(), clickText, Toast.LENGTH_SHORT).show();
}

The ListActivity will call this method (if you’ve defined it) every time an element in the list view is clicked. For more-complicated lists, you may want to use the ID—for example, if you are fetching your list contents from an SQLite database. For this simple demo, I’ve just popped up a little dialog showing the text of the item that was pressed. If you’re implementing your own basic main menu, I suggest you use the position of the clicked item to start an activity, service, or other action. You can see an example of this if you look at the associated source code.

That’s the most basic list view I could possibly show you. Now, I’ll take you in the opposite direction and show you what a custom list backed by a remote data source looks like.

Complex List Views

While building a main menu is great and all, there are much more complicated uses to which you can put the Adapter and ListView combination. In fact, I’m going to show you an example that gets complicated in two ways. First, the data source is going to be from a remote URL (a Reddit feed). Second, I’m going to add a second text view to the list (you could, if you want to, add any number of items to it).

The 1000-foot View

All right, here’s the game plan. First, you’ll need an AsyncTask to retrieve the feed from Reddit’s API. Once you have the data, you’ll need to parse it into JSON (JavaScript Object Notation) objects and feed that data into a custom adapter. Since this is a book on Android development and not JSON development, I’m going to leverage Google’s JSON parser, GSON. If you want to see how it works, the project is open source, but for the sake of this demo, you shouldn’t need to worry about it. Last, you’ll need to create that custom adapter and the specific ListView layout to hold the two pieces of text information. With those things in hand, you can create the custom layout object.

In the end, you’ll have a list of the most recently popular subreddits (nomenclature for groupings of similar content or interests) on Reddit, as well as an indicator of whether the subreddit accepts links, self-posts, or anything. These indicators will change color and letter for each respective type. This is by no means the most complex list you could build using these tools, but it is a great way to show you how to make your own complex custom list views.

Creating the Main Layout View

This step is very similar to the “Defining a Layout for Your ListActivity” section. You’ll need an XML layout containing a ListView with the android:id/list ID. In this case, however, because the data isn’t available when the activity launches, you’ll want the ListView to start out hidden. Here’s what my project’s XML layout looks like. (Don’t be alarmed by all the styling in this example; might as well make it look pretty, right?)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical">

   <TextView
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:background="#888888"
      android:fontFamily="sans-serif-light"
      android:gravity="center"
      android:padding="10dp"
      android:text="Popular on Reddit"
      android:textColor="#FFFFFF"
      android:textSize="24sp"
      android:textStyle="bold" />

   <RelativeLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent">

     <ProgressBar
        android:id="@+id/empty_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:indeterminate="true"
        android:text="Loading..."/>

     <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"/>
  </RelativeLayout>
</LinearLayout>

Creating the ListActivity

Again, you’re going to need a new ListActivity. Since you’re already good at getting them started, I’ll just skip to showing you what my onCreate method looks like:

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

   // What the user will see when the list is loading
   ProgressBar progress = (ProgressBar) findViewById(R.id.empty_view);
   getListView().setEmptyView(progress);

   mTask = new RedditAsyncTask();
   mTask.execute("http://www.reddit.com/subreddits/popular.json");
}


Image Note

If the Reddit URL in the earlier code listing isn’t working, I’ve stashed a backup copy of the data at http://wanderingoak.net/reddit_backup.json. If Reddit changes their API, you can always run the sample code against that URL.


The most important thing here is that we are executing the RedditAsyncTask, which kicks off the thread and starts the whole network process. Without execute(...), the task won’t actually run.

You’ll have noticed, if you were watching closely, that I created a private data member to contain the Reddit-fetching task. You astute readers might be wondering why I chose to stash it aside that way. The answer is that because this task isn’t happening on the main thread, I need to be able to cancel it should the user close down the activity before the task finishes. To do this, the onStop method will need to be able to call the Reddit-fetching AsyncTask, making it a private data member.

Getting Reddit Data

My first task, at least when it comes to doing work, is to load the stream of popular subreddits. You should, thanks to the previous chapter, be very familiar with the ins and outs of fetching network data, so a lot of this should seem straightforward. This is where that JSON parsing we talked about earlier comes into play. If you’re having a hard time grasping how GSON works, don’t worry about it! The more you work with it, the more sense it starts to make.

Without further preamble, here’s what my RedditAsyncTask looks like:

private class RedditAsyncTask extends
   AsyncTask<String, Integer, PopularSubreddit[]> {
   @Override
   protected PopularSubreddit[] doInBackground(String... params) {
     try {

       String url = params[0];
       HttpClient httpClient = new DefaultHttpClient();
       HttpGet post = new HttpGet(url);
       HttpResponse rp = httpClient.execute(post);

       if (rp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {

          String response = EntityUtils.toString(rp.getEntity());
          JsonParser parser = new JsonParser();
          // Turn response into a JsonObject
          JsonObject element = parser.parse(response).getAsJsonObject();
          // Get the children posts out of the JsonObject
          JsonElement jsonSubreddits =
            element.get("data").getAsJsonObject().get("children");
          Gson gson = new Gson();
          // Gson magically turns Json into the class you want!
          PopularSubreddit[] subreddits = gson.fromJson(jsonSubreddits,
             PopularSubreddit[].class);

           return subreddits;
         } else {
           return null;
         }
       } catch (Exception e) {
         e.printStackTrace();
         return null;
       }


   @Override
   protected void onPostExecute(PopularSubreddit[] subreddits) {
      if (subreddits!= null) {
          RedditAdapter adapter =
            new RedditAdapter(getBaseContext(),subreddits);
          setListAdapter(adapter);
      }
   }
}

Here are the general steps for fetching the data:

1. Fetch the data, and using your preferred JSON parsing method, turn the response into usable Java objects.

2. Once you’re back on the main thread inside the onPostExecute method, create a new adapter with all the subreddits that you have parsed, and set the adapter onto the list. Voilà! You’re done!


Image Note

Any changes to the Adapter’s data must take place on the main thread. Modifying the Adapter data counts as changing the UI, as far as Android is concerned. As always, all changes to the user interface must be carried out on the main thread. Keep this is mind as you create your own adapter, especially if you’re fetching data from the network off the main thread.


Making a Custom Adapter

All right, now comes the really interesting part. You need to create a custom Adapter to feed rows into the ListView.

Custom Adapters have four methods you are required to override, all of which allow the ListView to acquire information about your data set.

Image getCount() returns the number of rows currently in the set of information.

Image getItem(int position) returns an object corresponding to a particular row position.

Image getItemId(int position) returns the ID that corresponds to the item at a specific position. This is often used with Adapters that focus on Cursors (Android’s SQLite interfaces).

Image getView(int position, View convertView, ViewGroup parent) is where most of the Adapter’s work will take place. The ListView, in making this call, is essentially asking for the view at position. You must, in this method, return a correctly configured view for the data at position. More on exactly how this works in a minute.

As you can see by the get prefix on all the required methods, all that Android Adapters do is provide row content information to the ListView. The ListView, it would seem, is one very needy girlfriend (or boyfriend... I’m not sure how to assign gender to Android UI interfaces).

Let me show you the example before I talk about any more theory. Since earlier I parsed out all the popular subreddits into an array, I’m going to use this array as the data backing our adapter. This class is declared as a separate class in the same package as your activity.

public class RedditAdapter extends BaseAdapter {

   PopularSubreddit[] mSubreddits;
   Context mContext;

   public RedditAdapter(Context context, PopularSubreddit[] subreddits) {
      mContext = context;
      mSubreddits = subreddits;
   }

   @Override
   public int getCount() {
      if (mSubreddits == null) {
          return 0;
      } else {
         return mSubreddits.length;
      }
   }

   @Override
   public PopularSubreddit getItem(int position) {
      if (mSubreddits == null != position >= 0 &&
         position < mSubreddits.length) {
         return subreddits[position];
      } else {
         return null;
      }
   }

   @Override
   public long getItemId(int position) {
      return position;
   }
}

This code, for the most part, provides functions access to the PopularSubreddit data that is initialized in the constructor. It handles getting an item from a position. If there is no data (maybe it was cleared), then the Adapter simply reports that there’s nothing to see. This class extends from BaseAdapter because it contains all the baseline methods that I need to build my custom adapter.

Building the ListViews

At last you’ve come to the part where you get to build and return the individual custom list view elements. Here’s the code to do exactly that:

@Override
public View getView(int position, View convertView, ViewGroup parent) {

   PopularSubreddit subreddit = getItem(position);
   View view;

   //Reduce, Reuse, Recycle!
   if (convertView == null) {
       view = LayoutInflater.from(mContext).
         inflate(R.layout.list_item_reddit_popular, parent, false);

   } else {
     view = convertView;
   }

   TextView tv = (TextView) view.findViewById(R.id.header_text);
   String title = subreddit.data.title;
   tv.setText(title.toUpperCase());

   tv = (TextView) view.findViewById(R.id.sub_text);
   tv.setText(subreddit.data.public_description);

   TextView submissionTypeView = (TextView)
      view.findViewById(R.id.submission_view);
   if ("link".equals(subreddit.data.submission_type)) {
      submissionTypeView.setText("L");
      submissionTypeView.setBackgroundColor(0x77FF0000);

   } else if ("self".equals(subreddit.data.submission_type)) {
      submissionTypeView.setText("S");
      submissionTypeView.setBackgroundColor(0x7700FF00);

   } else if ("any".equals(subreddit.data.submission_type)) {
      submissionTypeView.setText("A");
      submissionTypeView.setBackgroundColor(0x770000FF);

   } else {
      submissionTypeView.setText("?");
      submissionTypeView.setBackgroundColor(0x77222222);
   }

   return view;
}

There are a couple of key points to consider in the getView code listing.

First, you need to figure out if the view can be recycled. If it can, you’ll reset all the visible values for it; otherwise, you’ll inflate a new row—by using the LayoutInflater—and configure it (more on how and why this works soon).

Second, you’ll get the title and description text from the Subreddit object and set them onto their respective TextViews.

Lastly, you will change the background color and text label of the submission type view to indicate to the user what kind of submissions this subreddit accepts. You might have noticed that I haven’t showed you what list_item_reddit_popular.xml looks like. That is the view layout I’m creating (by calling the inflate method and passing in the layout).

The custom layout view

This layout has three TextViews in it, with the IDs submission_view, header_text, and sub_text. These can be found in res/layout/list_item_reddit_popular:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:padding="10dp"
   android:orientation="vertical">

   <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:orientation="horizontal">

       <TextView
          android:id="@+id/accepted_submissions_view"
          android:layout_width="16dp"
          android:layout_height="16dp"
          android:fontFamily="sans-serif-condensed"
          android:gravity="center"
          android:layout_gravity="center_vertical"
          android:layout_marginRight="10dp"
          android:textSize="8sp" />

       <TextView
          android:id="@+id/header_text"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_gravity="center_vertical"
          android:fontFamily="sans-serif-condensed"
          android:textSize="18sp"
          android:textColor="#444444" />
   </LinearLayout>

   <TextView
     android:id="@+id/sub_text"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:maxLines="3"
     android:textColor="#666666" />
</LinearLayout>

With this layout, you now have all the moving pieces you need to download, parse, and display the most popular subreddits feed. Figure 5.2, at last, is what the popular subreddit viewer looks like in ListView form.

Image

Figure 5.2 The list of the most popular subreddits!

How Do These Objects Interact?

To understand how the ListView interacts with the Adapter, there are a few constraints you must understand. First, lists could scroll on to infinity, at least from the point of view of the device. Yet, as you might have guessed, the phone has a limited amount of memory. This means that not every single list item can have its own entry in the list, because the device would quickly run out of space. Further, if the ListView had to lay out every single row right up front, it could be bogged down for an unacceptable amount of time.

What Android does to solve these problems is to recycle list element rows. The process looks a little bit like this:

1. Android goes through the entire list, asking each row how large it would like to be (this is so it knows how large to draw the scroll indicator).

2. Once it knows roughly how big the entire ListView will be, it then requests views for the first screen, plus a buffer (so it won’t have to stop and get more when the user starts scrolling). Your adapter will have to create, configure, and return those views as the ListView calls getView over and over again.

3. As the user scrolls down and rows fall off the top of the list, Android will return them to you when it calls getView. Effectively, it’s asking you to reuse a previous view by passing in the convertView object to you.


Image Note

Any asynchronous task, such as loading an icon from disk or loading a user’s profile icon, must check that the ListView hasn’t recycled the view while it’s been downloading or loading the image data. If the row is still showing the same data when the task finishes, it’s safe to update the row; otherwise, it needs to cache or chuck the data.


More Than One List Item Type

Sometimes, you will be displaying list contents that have more than one view type. For example, you might be displaying content that has images interspersed with text. While you could certainly create a row item that contains all the items and hide or show the relevant parts, it might make more sense to have separate view types to display them.

public class MultiTypeAdapter extends BaseAdapter{
   private static final int TYPE_TEXT = 0;
   private static final int TYPE_IMAGE = 1;

   @Override
   public int getViewTypeCount() {
     return 2;
   }

   @Override
   public int getItemViewType(int position) {
      if (getItem() instance String) {
         return TYPE_TEXT;
      } else {
         return TYPE_IMAGE;
      }
    }
}

By returning 2 from getViewTypeCount(), we are letting the adapter know that there are two different views we are recycling, so please hold on to both of them. Then, when the system tries to determine which recycled view to give you, it calls getItemViewType() and asks you to tell it what type of view should be used for the data at the given position. There is no limit to how many different view types you can have in an adapter, but keep in mind that the more you have, the more it will keep in memory.

Wrapping Up

This chapter covered the basics of both simple and custom ListViews and Adapters. I showed you how to create a simple main menu, and I walked you through a simple example of building a custom Adapter to handle a more complex ListView. You now have a grasp of the basics.

Lists are still one of the cornerstones of mobile development. I advise you, however, to make as few boring, graphically flat lists as you possibly can. While these examples are great for showing you how to build lists of your own, they are by no means shining examples of solid interface design. You can, and very much should, make lists when needed, but dress them up as much as you can without affecting performance.

If you’re hungering for more, I highly suggest reading through Android’s implementation of ListActivity.java or ListFragment.java. Because Android is open source, you can get all the code that makes up its SDK for free! Head over to http://source.android.com for more information.

Lastly, I wrote more code for this chapter than I had space to explain here. I recommend checking out the sample code associated with this chapter (at Peachpit.com/androiddevelopanddesign) to learn more about launching a screen as the result of a menu click and about how to build a similar main menu screen using a ListFragment.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset