Android Tutorial for Beginners: Part 3

An Android Tutorial that shows you how to make your first app app step-by-step, using Android Studio! By Darryl Bayliss.

Leave a rating/review
Save for later
Share
You are currently viewing page 4 of 6 of this article. Click here to view the first page.

Putting Together the Insta-Row

The last method, getView, answers the ListView when it comes to the adapter and asks: What should I show at position X?

To begin to answer that question, you first need to create what’s called a view holder. Add the following to the end of your JSONAdapter code (but before the final closing curly brace):

// this is used so you only ever have to do
// inflation and finding by ID once ever per View
private static class ViewHolder {
    public ImageView thumbnailImageView;
    public TextView titleTextView;
    public TextView authorTextView;
}

This class is simply a packager of the three subviews that every row in your list will have. Think of it as a Do-It-Yourself kit for your list cells. All each row needs to do is get one of these, update it with the right data based on the row and presto: an Insta-Row!

The trick is that as you scroll around through who-knows-how-many books in your list, the app shows the data using the same cells, over and over. There are only just enough list cells to fill the screen, plus a few extras. Keeping all of the list cells in memory, even while they’re off-screen, would get crazy!

As a view scrolls out of sight, the recycling crew comes by and dumps out everything inside the view, but hangs onto the ViewHolder. That same view, and the ViewHolder, then get handed over to a list cell about to scroll into sight.

recycle_viewholders

The re-used view is handed one of these ready-made Insta-Row kits (aka a ViewHolder), and simply fills the contents of each subview as needed, rather than inflating a brand new view from XML and creating all those subviews from scratch every single time.

For more details on the view recycling process, here is a helpful blog post about it.

With that in mind, replace the stub for getView with this code:

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

    // check if the view already exists
    // if so, no need to inflate and findViewById again!
    if (convertView == null) {

        // Inflate the custom row layout from your XML.
        convertView = mInflater.inflate(R.layout.row_book, null);

        // create a new "Holder" with subviews
        holder = new ViewHolder();
        holder.thumbnailImageView = (ImageView) convertView.findViewById(R.id.img_thumbnail);
        holder.titleTextView = (TextView) convertView.findViewById(R.id.text_title);
        holder.authorTextView = (TextView) convertView.findViewById(R.id.text_author);

        // hang onto this holder for future recyclage
        convertView.setTag(holder);
    } else {

        // skip all the expensive inflation/findViewById
        // and just get the holder you already made
        holder = (ViewHolder) convertView.getTag();
    }
    // More code after this

    return convertView;
}

If it happens to be the first time for the view, then you need to use your custom row XML using mInflater and find all your subviews using findViewById. But as mentioned earlier, the view might already exist — in which case you want to skip all that from-scratch stuff.

You use the setTag and getTag methods to hang onto the ViewHolder and easily pack/unpack it while scrolling around.

Next, you need to handle the image thumbnail of the book’s cover. Put this new code right after the // More code after this comment line:

// Get the current book's data in JSON form
JSONObject jsonObject = (JSONObject) getItem(position);

// See if there is a cover ID in the Object
if (jsonObject.has("cover_i")) {

    // If so, grab the Cover ID out from the object
    String imageID = jsonObject.optString("cover_i");

    // Construct the image URL (specific to API)
    String imageURL = IMAGE_URL_BASE + imageID + "-S.jpg";

    // Use Picasso to load the image
    // Temporarily have a placeholder in case it's slow to load
    Picasso.with(mContext).load(imageURL).placeholder(R.drawable.ic_books).into(holder.thumbnailImageView);
} else {

    // If there is no cover ID in the object, use a placeholder
    holder.thumbnailImageView.setImageResource(R.drawable.ic_books);
}

In this section, you first get the JSONObject for the precise book whose data you want to display. Of course, this is dependent on the item’s position in the list.

Next, you check to see if there’s a cover ID for that book. Unfortunately, many books don’t have covers in the Open Library database. So, you look to see if a cover is there by calling has("cover_i"), which returns a true-or-false boolean. If it returns true, then you parse out the cover ID from the JSONObject and use it to construct a URL specific to Open Library.

You can change the “-S.jpg” to “-L.jpg” for a larger version of the same image: http://covers.openlibrary.org/b/id/6845816-L.jpg

Note: An example URL from this operation: http://covers.openlibrary.org/b/id/6845816-S.jpg

Once you have the URL, you simply tell Picasso to download it and display it in your ImageView. You also specify a placeholder image to show while the cover image is downloading.

If the book doesn’t have a cover assigned, you show the standard icon.

Finally, you need to populate the book title and author name. So, add the following code immediately after the block of code you added above:

	// Grab the title and author from the JSON
String bookTitle = "";
String authorName = "";

if (jsonObject.has("title")) {
    bookTitle = jsonObject.optString("title");
}

if (jsonObject.has("author_name")) {
    authorName = jsonObject.optJSONArray("author_name").optString(0);
}

// Send these Strings to the TextViews for display
holder.titleTextView.setText(bookTitle);
holder.authorTextView.setText(authorName);

This step is similar to the last. As long as the JSONObject contains the title and author name, you parse the values and set the text of each TextView!

Connecting the List to the Adapter

The last thing you need to do before you can test your newly-webified app is connect the ListView to the JSONAdapter.

Remove the following code from onCreate in MainActivity.java:

// Create an ArrayAdapter for the ListView
mArrayAdapter = new ArrayAdapter(this,
		android.R.layout.simple_list_item_1,
		mNameList);

// Set the ListView to use the ArrayAdapter
mainListView.setAdapter(mArrayAdapter);

Also remove the following from onItemClick in MainActivity.java:

        // Log the item's position and contents
        // to the console in Debug
        Log.d("omg android", position + ": " + mNameList.get(position));

You don’t need any of that simple stuff now that you’ve got your own souped-up Adapter!

Now, to start using the your adapter class, replace this line at the beginning of MainActivity.java:

ArrayAdapter mArrayAdapter;

With this:

JSONAdapter mJSONAdapter;

Next, add the following to the end of onCreate:

// 10. Create a JSONAdapter for the ListView
mJSONAdapter = new JSONAdapter(this, getLayoutInflater());

// Set the ListView to use the ArrayAdapter
mainListView.setAdapter(mJSONAdapter);

Great! You just created an instance of your snazzy new JSONAdapter, feeding it a Context and a LayoutInflater. Your Activity can be used as a Context parameter (via the this keyword) because Activity is a Subclass of Context. Now your adapter is hooked up and can provide your ListView with the data it needs.

If you were to build and run, though, you would be rather underwhelmed by the results. Even after inputting a search String, the ListView remains empty. Why?

Because, if you recall, you created your Adapter using its constructor, public JSONAdapter(Context context, LayoutInflater inflater). That method creates an empty JSONArray as a placeholder.

An empty list is OK to start with, of course, but it sure would be great to update the list when your search is done! That’s not happening yet, so that’s next on your agenda.