Make Your First Android App: Part 3/3

Build upon the foundations of the first two part on how to make your first Android app and create a book search application using web APIs. By Matt Luedke.

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

Adapting JSON for a ListView

In Part Two, you made a ListView, at which point I mentioned that ListViews are a bit picky. They don’t want to deal with the data directly — you can hardly blame them after seeing that confusing JSON response that popped into LogCat earlier. The simple, built-in adapter you used in Part Two won’t cut it here; you need a custom one.

Right-click on the com.example.omgandroid folder (or whatever package name you used when creating the project) and select New > Java Class.

new_java_class

Then type in JSONAdapter as the new class name.

create_jsonadapter

Once you have your new class open in the editor, add the following code so the class looks like this:

public class JSONAdapter {

	private static final String IMAGE_URL_BASE = "http://covers.openlibrary.org/b/id/";

	Context mContext;
	LayoutInflater mInflater;
	JSONArray mJsonArray;

	public JSONAdapter(Context context, LayoutInflater inflater) {
		mContext = context;
		mInflater = inflater;
		mJsonArray = new JSONArray();
	}
}

This is still just a basic class, beginning with the first part of the URL you’ll use to download images — more on that when you implement the image download code.

Next, there are three simple variables:

  • A Context. This is a complex topic, but basically you only need it to tell Picasso, the image downloader library, what is going on in the app when you tell Picasso to get working.
  • A LayoutInflater. You need this to inflate a View out of that list item XML you just wrote.
  • A JSONArray. This is the datasource that will be coming in from the server in response to your query!

The JSONAdapter method is the class constructor – that’s what you call when you create a new instance of JSONAdapter. So, anyone who wants to ask JSONAdapter to do anything has got to create an instance of it first, which in turn requires submitting the Context and LayoutInflater via the constructor.

The constructor currently simply saves the passed in references and creates an empty JSONArray. You’ll pass the real data to the class after the search results are in.

Now you need to convert this class into an actual Adapter class. This is quite easy in an object-oriented programming language like Java – simply change the top line of the class from:

public class JSONAdapter {

To:

public class JSONAdapter extends BaseAdapter {
Note: Those with a fuzzy grasp of object inheritance and other object-oriented programming concepts may want a refresher like this one, but in essence, you’re saying that JSONAdapter is going to build on the basics provided by the BaseAdapter class.

Right away, Android Studio will underline the line you just modified in red to let you know that you need to add more to JSONAdapter before it accurately extends BaseAdapter. Studio isn’t just a naysayer, though — it can help, too! Click underlined line, then click the red light bulb that pops up next to it, and then select Implement Methods from the menu.

implement_baseadapter_methods

When asked to select methods to implement, make sure all four methods are highlighted and click OK.

select_methods_baseadapter

Magically, Android Studio creates four methods for you and the red underlining disappears. This means that you’ve satisfactorily extended BaseAdapter.

But… all the methods are empty. It’s time to go through each one in turn and make them do what you want.

So, first replace the current implementation for getCount with the following:

@Override 
public int getCount() {
	return mJsonArray.length();
}

getCount answers the question: How long does your ListView need to be? In this example, the answer is simply the length of your JSONArray. Each entry in that array represents a book and so each one gets a row in the ListView.

Now replace getItem with this version:

@Override 
public JSONObject getItem(int position) {
	return mJsonArray.optJSONObject(position);
}

getItem returns the book for a given position, counting up from 0. A single book is represented by a JSONObject. They all just happen to be store in a JSONArray. So all you have to do is look through the array for the JSONObject at the given position.

Next, replace the stub for getItemId with this code:

@Override 
public long getItemId(int position) {
	// your particular dataset uses String IDs
	// but you have to put something in this method
	return position;
}

This can be a very helpful method in some situations, but in this case, you don’t really need it. So, you just set it to position. Imagine a situation where you have a list of books, as a subset of a larger database, and each book has an ID in the larger database. If you needed to go back and query for more information based on a certain item’s ID number, this method would be helpful for you.

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!

Matt Luedke

Contributors

Matt Luedke

Author

Over 300 content creators. Join our team.