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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Make Your First Android App: Part 3/3
50 mins
- Getting Started
- Networking Considerations
- A Glance at Gradle
- JSON Basics
- Creating a Query
- Making the API Call
- Creating the List Rows
- Adapting JSON for a ListView
- Putting Together the Insta-Row
- Connecting the List to the Adapter
- Updating the List Data
- Showing Progress
- The Detail Activity
- The Up and Back Buttons
- An Intent to Show the Detail Activity
- Sharing the Image
- Where to Go From Here?
Adapting JSON for a ListView
In Part Two, you made a ListView
, at which point I mentioned that ListView
s 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.
Then type in JSONAdapter as the new class name.
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 aView
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 {
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.
When asked to select methods to implement, make sure all four methods are highlighted and click OK.
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.
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
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
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
!