This tutorial is the third and final part of the series devoted to helping you make your first Android app! Check out Part One to get started with Android and Part Two to learn more about Android UI and project structure.
In this final part of the tutorial, you’ll learn how to leverage the powerful capabilities of the web to search and display data and images. More specifically, you’ll make an app that searches the Open Library API — a database of over 20 million books —, displays information and cover images of the books it finds, and allows you to recommend books to friends!
When you’re done, you’ll know how to:
- Add powerful third-party libraries to your app via Gradle;
- Access a typical RESTful API;
- Understand JSON format and parse JSON results;
- Responsibly download web images without affecting your UI performance;
- Customize list cells with multiple views; and
- Build intuitive native app navigation between activities.
These are very useful and transferable skills for all sorts of Android apps you’ll want to make in the future.
To begin this part of the tutorial, you should be at the point where you have an app that takes user input, lists names, and shares messages through social networks. Your app should also ask for your name when you first open the app and greet you by name thereafter.
So the personal part is done — now it’s time to interwebify!
Getting Started
It’s time to do a bit of rebranding. No longer is this just a little demo app — it is a book search and recommendation engine!
The default Android icon can only take you so far. Download the following files and drag them onto the src/main/res/drawable-hdpi directory to add them to your project:
Click OK when asked to confirm the move.
Your src/main/res/drawable-hdpi directory should now look something like this:
hdpi
assets are provided here. For future projects, it is a good idea to add assets for the other dpi values as well.Open AndroidManifest.xml. As you recall, this is “the boss” of your app. If you want to change the app’s icon, you need to talk to the boss.
Find the opening application
tag and change the icon
attribute line from:
android:icon="@drawable/ic_launcher" |
To:
android:icon="@drawable/ic_books" |
From now on, the app icon will be a stack of books instead of the default Android icon. Depending on your Studio version (and/or SDK version) you may also see that the application
has an attribute for setting the application name. You’re going to update the application name as well, but not in the manifest.
First, while you’re still looking at the manifest, you need to let the manifest know that you plan to start accessing the Internet. Between the uses-sdk
and application
tags, add the following:
<!-- NEED TO ADD TO BE ABLE TO GO ONLINE AND GET DATA --> <uses-permission android:name="android.permission.INTERNET"/> |
If you don’t let “the boss” know your plans, it won’t file the appropriate paperwork with Android to make the web call happen. But now you’re good to go.
Now it’s time to change your app’s name. Open res/values/strings.xml and replace the strings for everything except action_settings
with the following new values:
<string name="app_name">Bookmaster General</string> <string name="textview">Search For Books!</string> <string name="button">Search</string> <string name="hint">Title and/or Author</string> |
That’s right — I used the title Bookmaster General.
Finally, remove this line from onCreate
in MainActivity.java:
mainTextView.setText("Set in Java!"); |
Run your app, you should see the new icon in the top left corner and the new application name on the Action Bar.
Your device or emulator’s home screen should also reflect the name and icon updates. For example:
Now that you’ve rebranded your app, it’s time to start adding the web interactions!
Networking Considerations
There are a lot of things to keep in mind when adding networking capabilities to your app. For example, you need to consider how to keep all the network interactions off the UI thread, so that the user can continue to use your app without everything locking up until a download completes. If you were using an app and it became completely unresponsive for lengths of time, you’d get pretty frustrated!
Thankfully, you can solve issues such as this by simply using third-party libraries. These have been specially designed and supported by Android experts to facilitate networking. Some of the most well-regarded include Retrofit, Volley, and Android Async Http. For the purposes of this tutorial, you’ll use Android Async Http.
Image downloads are also a consideration since each image takes time to download. Also, in the case of a list of books, if you have your list set to download images as needed, you might find that you end up downloading the same image over and over as your user scrolls through the list of items. You really don’t want that type of behavior. You’ll use another third-party library called Picasso to manage image downloads for you.
A way to easily manage your project’s dependencies would sure be great! That leads us to Gradle.
A Glance at Gradle
When you created your project in Android Studio, you may remember mentions of Gradle and Maven. Refer to Part One if you need a reintroduction. Now you’ll see them in action.
Open OMGAndroid/build.gradle. Note that there are two build.gradle
files in your project. You want the one that’s within the OMGAndroid
folder – not the one at the project root level:
Note: The contents of your build.gradle
file might not look exactly the same as the screenshot above. Android Studio is still in a state of flux and depending on the changes to Studio since the tutorial, the contents of the build.gradle
file might be slightly different.
Some of the stuff happening in here is beyond the scope of this tutorial, but if you become serious about Android development, I recommend looking into Gradle a little more, starting of course, with the Gradle website. For now, just notice that the Android plugin is being applied to the project (apply plugin: 'android'
).
You might also see a reference to the Maven Central Repository (mavenCentral()
) but depending on on Android Studio changes, the Maven Central references could also have moved to the other build.gradle
file. Suffice it to say that if you see a line similar to mavenCentral()
, that’s setting up a reference to the Maven Central Repository.
Scroll down to the area labeled dependencies
. There might be two places this word appears (again, depending on the Android Studio version); you want the one that at the end of the file, after the android
section. There may already be a support library listed there, or it may be empty. You’re going to add two new libraries.
Add the libraries like this:
dependencies { ... // there may or may not be a support library above these compile 'com.loopj.android:android-async-http:1.4.4' compile 'com.squareup.picasso:picasso:2.1.1' } |
Then find the Sync Project with Gradle Files button on the Studio toolbar and press it. It looks like this:
Believe it or not, that’s it! Just like that, you’ve included the Android Async Http and Picasso libraries in your project and you can start using them whenever you like.
It’s so easy because both of these libraries are available via the Maven Central Repository, to which your project already contains a reference. So, when you tell Gradle which libraries you’d like to use, it simply grabs them from the source and you’re good to go.
If you need to include any libraries which are not available on the Maven Central Repository, then you’d still have to go through the old school method of copying the source (or the library) into your project. But most of the time, you won’t have to go through all that pain since the Maven Repository contains a lot of third-party libraries for you to use. So you’ll probably be able to find an alternative to the library you’re interested in. If interested, you can even browse all the libraries available on the Maven Repository.
JSON Basics
Great! Now it’s time to meet your datasource: the Open Library API. It’s a constantly-updated database of books, searchable by author and title. The wealth of data is enormous!
Try this query as an example: http://openlibrary.org/search.json?q=hunger+games+suzanne+collins
This is a relatively simple URL to understand; whatever is typed in after the ?q=
is the query string that will be used to search the database. Feel free to change it to a different author name/title and compare results, remembering to use +
to separate words.
The result is in the form of a large JSON response. Take a minute to look around at the sort of data the response includes.
If you’re not familiar with the JSON format, I suggest a quick glance through the JSON page. The basics are that there are two ways data can be arranged in JSON: arrays and objects.
JSONArrays
list objects of the same type. For example, a list of books might look like this:
["The Hunger Games", "Harry Potter and the Sorcerer's Stone", "A Game Of Thrones"]
JSONObjects
are a collection of key-value pairs. For example, a very simple object describing a book might be:
{"title" : "The Hunger Games", "author_name" : "Suzanne Collins"}
The results from your queries to the Open Library API are really just expansions on those two basic structures. They are larger, and nested into several levels, but the ideas remain the same.
Creating a Query
Now that you know roughly what to expect from the datasource, it’s time to set up the code to go make a sample query!
Add the following variable at the top of MainActivity.java:
private static final String QUERY_URL = "http://openlibrary.org/search.json?q="; |
The above is simply a reference to the URL you’ll be calling. It’s much better to hold this URL as a static String
, so you don’t have to go searching through your code for it. Remember that you’re going to append the search string after the ?q=
.
Next, add this new method:
private void queryBooks(String searchString) { // Prepare your search string to be put in a URL // It might have reserved characters or something String urlString = ""; try { urlString = URLEncoder.encode(searchString, "UTF-8"); } catch (UnsupportedEncodingException e) { // if this fails for some reason, let the user know why e.printStackTrace(); Toast.makeText(this, "Error: " + e.getMessage(), Toast.LENGTH_LONG).show(); } // Create a client to perform networking AsyncHttpClient client = new AsyncHttpClient(); // Have the client get a JSONArray of data // and define how to respond client.get(QUERY_URL + urlString, new JsonHttpResponseHandler() { @Override public void onSuccess(JSONObject jsonObject) {} @Override public void onFailure(int statusCode, Throwable throwable, JSONObject error) {} }); } |
queryBooks
handles the network call to the API. It encodes the input searchString
into URL format, then appends that to the base URL you specified at the top of the class.
Calling new AsyncHttpClient()
simply creates an instance of the HTTP client. It’s got a lot of great methods built-in, but the only one you need here is get(String url, ResponseHandlerInterface responseHandler)
.
The get
method takes in two parameters:
String url
is simply the URL from which you’d like to fetch data. You supply an input made of the base URL defined at the top of the class, plus the search string.JsonHttpResponseHandler
, which you define now, even though you don’t know whether the network call will succeed or fail, or how long it will take either way. It contains methods calledonSuccess
andonFailure
to respond to the two cases once the handler does get a response from the server.
You might have noticed that onSuccess
and onFailure
are currently method stubs with no code. Fix that by fleshing out the onSuccess
to match the following:
@Override public void onSuccess(JSONObject jsonObject) { // Display a "Toast" message // to announce your success Toast.makeText(getApplicationContext(), "Success!", Toast.LENGTH_LONG).show(); // 8. For now, just log results Log.d("omg android", jsonObject.toString()); } |
Next, implement onFailure
as follows:
@Override public void onFailure(int statusCode, Throwable throwable, JSONObject error) { // Display a "Toast" message // to announce the failure Toast.makeText(getApplicationContext(), "Error: " + statusCode + " " + throwable.getMessage(), Toast.LENGTH_LONG).show(); // Log error message // to help solve any problems Log.e("omg android", statusCode + " " + throwable.getMessage()); } |
In both cases, you simply present a Toast
and log the results. Soon, though, the success case will get a lot more exciting.
Making the API Call
queryBooks
is a great method and all, but it still needs to be hooked up to the EditText
and Button
to complete the search capability.
Fortunately, that’s pretty simple. In MainActivity.java, find onClick
and replace the current implementation with this:
// 9. Take what was typed into the EditText and use in search queryBooks(mainEditText.getText().toString()); |
Now, every time the user taps the button, this method takes the user’s input in the EditText
control and queries the Open Library for books and authors matching that string. Run your app and try it out to see what happens!
NoClassDefFoundError
upon querying. If so, not to worry — simply select Build > Rebuild Project and try again!Remember that you haven’t yet hooked up the results to anything that will display them on the screen. But open LogCat and you can see the resulting JSON spilled out whenever the API call finishes.
This is already exciting and clearly has the potential to be something cool very soon! But it’s still just a jumble of data. Your next challenge, then, is to display this data on the screen in a more organized manner.
Creating the List Rows
First, you need to set up the layout for each of the rows in your list. A simple row of text won’t cut it anymore — you need space for a thumbnail image on the left and then two rows of text for the title and author, respectively. A layout which would look something like this:
Right-click on the res/layout folder in the Studio left pane, and select New > Layout resource file.
Name your file row_book.xml with a root element of RelativeLayout
.
The new file will open in Design mode. So, switch to Text mode, as before. Now change the layout_height
attribute from this:
android:layout_height="match_parent" |
To this:
android:layout_height="75dp" |
You simply set the layout to have a specific height instead of matching the height of the parent container.
Next, you’re going to add three views inside the RelativeLayout
, and then you’ll witness a few of the capabilities of this type of layout in action. First, add the thumbnail view:
<ImageView android:id="@+id/img_thumbnail" android:layout_width="50dp" android:layout_height="50dp" android:layout_marginLeft="25dp" android:layout_alignParentLeft="true" android:layout_centerVertical="true" android:scaleType="centerInside"/> |
The ImageView
has the width and height set and there’s a little margin to the left of the picture. This is stuff you’ve seen before.
Then it gets interesting with layout_alignParentLeft
and layout_centerVertical
. These attributes are available since this ImageView
is a child of a RelativeLayout
. Because of these two attributes, the ImageView
stays tight to the left of the cell with that margin intact, and centers vertically.
The last attribute, scaleType
, specifies how you’d like the image to display within the amount of space it’s given. Especially given the unpredictable list of screen sizes you’d have to support, it’s often important to set this beforehand.
Using centerInside
means that you’ll preserve the aspect ratio of the image and that both dimensions will fit inside the given space. You might, however, have some blank space above and below the image if it’s too short, or space on the sides if it’s too thin. If you’re interested, you can read up on the various ScaleType
options.
Next, add a TextView
for the book’s title:
<TextView android:id="@+id/text_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="25dp" android:layout_toRightOf="@+id/img_thumbnail" android:layout_alignTop="@+id/img_thumbnail"/> |
Read through the XML first and see if you can tell what’s going on. The commands should be starting to make a bit of sense by now. The only thing that might give you pause might be the @+id/img_thumbnail
bit – but that’s just a reference to another control by ID. In this case, the ID refers to the ImageView
you added previously.
Basically, the title will sit to the right of the thumbnail, such that the top of the title will be at the same height as the top of the thumbnail. There’s some space in between the two controls, as well.
Finally, add the TextView
for the author’s name:
<TextView android:id="@+id/text_author" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/text_title" android:layout_alignLeft="@+id/text_title"/> |
By now, these attributes should all make sense. The author’s name will be below the title, with its left side aligned to that of the title.
RelativeLayout
s often reference other children using IDs, as you saw just now. Just make sure that the references don’t get circular (two attributes that depend on each other), or else your XML won’t inflate!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
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:
// 5. 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 the class:
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
. 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.
Updating the List Data
To update the list, you need to add an update method to your adapter and then call it from your activity.
First, add the following method to JSONAdapter.java:
public void updateData(JSONArray jsonArray) { // update the adapter's dataset mJsonArray = jsonArray; notifyDataSetChanged(); } |
This method accepts a JSONArray
input, sets it as the adapter’s datasource, and calls notifyDataSetChanged
to refresh the list. The adapter is already set up to know what to do with the data, so that’s all you need!
Now go back to MainActivity.java. You’re going to use your new method to update the list when the network call comes back. Find the following code in onSuccess
, which is embedded within queryBooks
:
// 8. For now, just log results Log.d("omg android", jsonObject.toString()); |
Replace it with this instead:
// update the data in your custom method. mJSONAdapter.updateData(jsonObject.optJSONArray("docs")); |
This is simply a call to updateData
with the newly-returned query response. As soon as the data comes back, you don’t waste any time — you send it straight to the adapter, which whips it into shape for the ListView
!
It’s finally time — run your app, and search away!
Now you can type a search string into your EditText
, tap the Search button and let the ListView
(somewhat) magically populate from your web search. Not only that — you can scroll through all the results and look at book titles, author names, and thumbnails of the cover images. This is already a pretty cool app!
Showing Progress
One nice feature you may notice missing is some kind of progress bar or spinner to let the user know your app is “thinking.” There’s a convenient place to show one on Android — the Action Bar.
Add the following to onCreate
in MainActivity.java, before everything else in the method except the call to super
:
// 11. Add a spinning progress bar (and make sure it's off) requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); setProgressBarIndeterminateVisibility(false); |
This whole request
business simply gives you easy access to the progress bar when you need it. The place to turn it on is right as your network query begins, so add this line to queryBooks
, immediately after creating your AsyncHttpClient
:
// 11. start progress bar setProgressBarIndeterminateVisibility(true); |
You want the spinner to stop when the request is over, which could actually be in one of two spots – onSuccess
or onFailure
. Add the following line at the very beginning of onSuccess
:
// 11. stop progress bar setProgressBarIndeterminateVisibility(false); |
And add the same line to the beginning of onFailure
:
// 11. stop progress bar setProgressBarIndeterminateVisibility(false); |
Now, run the app again and do another search.
This time, you’ll notice a progress indicator pop up and start spinning in the Action Bar as your networking calls happen. Much better!
The Detail Activity
Seeing the list of all the books is exciting! The next logical step is to let the user select a book from the list to see more details or a larger version of the cover.
For this app, you’ll only show a larger version of the cover. But the techniques you use will pave the way for the additional challenge of displaying any further details that may interest you. Let’s get started!
First, add the following line to res/values/strings.xml:
<string name="activity_details">Book Details</string> |
This is simply to provide a title for the activity. As mentioned before, it’s good to keep all the strings in one file!
Next is the layout XML. It won’t be complicated. Right-click on res/layout and select New > Layout Resource File.
Name it activity_detail.xml, with a Root Element of ImageView
.
That’s almost it right there. All you need to do now is give the ImageView
an id
, a default image, and a bit of margin space for good measure. Edit activity_detail.xml to look like this:
<?xml version="1.0" encoding="utf-8"?> <ImageView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/img_cover" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="25dp" android:src="@drawable/img_books_large"/> |
That’s simple enough. This screen will now show a single ImageView
with a 25dp margin all around, and the default image is img_books_large
.
Next, you need to make a new Activity
. Right-click on the com.example.omgandroid package (or the package name you set originally) and select New > Java Class, as before.
Name the class DetailActivity.
This creates a simple, empty class for you. Next, modify the class definition so that your new class extends Activity
, like this:
public class DetailActivity extends Activity { |
You already know you need to access the ImageView
from your layout, so add the following method to your activity:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Tell the activity which XML layout is right setContentView(R.layout.activity_detail); // Enable the "Up" button for more navigation options getActionBar().setDisplayHomeAsUpEnabled(true); // Access the imageview from XML ImageView imageView = (ImageView) findViewById(R.id.img_cover); } |
The above code simply tells the Activity
to use the simple XML layout you made earlier, and then grabs the ImageView
you need from it. But wait: what is that getActionBar
stuff doing in there?
The Up and Back Buttons
You may have noticed, or simply used it without really thinking about it, the Up button in many Android apps. The proper Android design of the Up and Back buttons is well-documented here and is worth a read, but it all begins with enabling the button as you just did.
The other steps for enabling the Up button take place in your manifest. Open AndroidManifest.xml and add the launchMode
attribute to MainActivity
so that it looks something like this:
<activity android:name="com.example.omgandroid.MainActivity" android:label="@string/app_name" android:launchMode="singleTop"> |
Note: Depending on your Android Studio version, the package name you selected when you created the project, and a few other factors, the above might not match what you see in your own AndroidManifest.xml
file exactly. The only thing you need to really worry about is adding the new launchMode
attribute as shown above. You can leave the rest as is.
So, do you remember my whole spiel about how the manifest is “the boss” who takes in “jobs” in the form of Intents
and checks if there is a team member right for the task? Normally, the manifest would arrange for the system to start a brand-new instance of that Activity
every time.
But, by setting the launchMode
attribute to singleTop
, you’re telling the manifest to use an already-existing instance of that Activity
, if possible. That way, when you use either the Back or Up button to return to the main screen, your most recent search results will still be there and you won’t have to start from scratch!
Next, add a definition for the DetailActivity
to the manifest, immediately after the one for MainActivity
:
<activity android:name=".DetailActivity" android:label="@string/activity_details" android:parentActivityName=".MainActivity"> <meta-data android:name="android.support.PARENT_ACTIVITY" android:value=".MainActivity"/> </activity> |
This looks pretty similar to the definition for MainActivity
, except for the stuff about “parent activity.” Setting the parent activity tells the manifest which activity should be displayed when there’s a request to go up from DetailActivity
.
An Intent to Show the Detail Activity
Now that you’ve set up your manifest to be aware of your DetailActivity
, you need to send it an Intent
to start it! This Intent
is going to originate from the MainActivity
, when the user selects a book from the list.
If you recall, the method that executes when a cell is selected from the ListView
is onItemClick
in MainActivity.java. It used to log information originally but now is empty. Add the following code to it:
// 12. Now that the user's chosen a book, grab the cover data JSONObject jsonObject = (JSONObject) mJSONAdapter.getItem(position); String coverID = jsonObject.optString("cover_i",""); // create an Intent to take you over to a new DetailActivity Intent detailIntent = new Intent(this, DetailActivity.class); // pack away the data about the cover // into your Intent before you head out detailIntent.putExtra("coverID", coverID); // TODO: add any other data you'd like as Extras // start the next Activity using your prepared Intent startActivity(detailIntent); |
Here, you create an Intent
to take you from where you are now (this
) to an instance of DetailActivity
. But before you fire off the command using startActivity
, there’s one more thing to remember to pack away.
As you’ve previously seen when you created setShareIntent
, you can pack “extras” into an Intent
in the form of key-value pairs. Since your DetailActivity
needs to know the cover ID to display, you extract that ID from the book’s JSON data and send it along.
TODO
reminder for an optional challenge to you. If you want your DetailActivity
to do anything more than show an image, you should send along additional data here.Build and run your app, and you will be able to click on a list item from your search results to see your new DetailActivity
! You can also navigate back to the main screen using either the Up or Back button.
Right now, you only see the placeholder image, but you know where this is headed :]
Add the following variables at the beginning of DetailActivity.java (right after the class definition line):
private static final String IMAGE_URL_BASE = "http://covers.openlibrary.org/b/id/"; // 13 String mImageURL; // 13 |
This sets the base URL for cover images on the Open Library API. You also create mImageURL
to hang onto any specific URL so that different methods within your Activity
can use it without needing to create the image URL all over again each time.
Next, add the following code to the end of onCreate
:
// 13. unpack the coverID from its trip inside your Intent String coverID = this.getIntent().getExtras().getString("coverID"); // See if there is a valid coverID if (coverID.length() > 0) { // Use the ID to construct an image URL mImageURL = IMAGE_URL_BASE + coverID + "-L.jpg"; // Use Picasso to load the image Picasso.with(this).load(mImageURL).placeholder(R.drawable.img_books_loading).into(imageView); } |
The above code digs into the Intent
that brought you to this Activity
and sees if it contains a String
with the name coverID
. If so, you proceed to use Picasso to download the image, just as you did in all the row cells. You display a “loading” image until the desired image is ready.
Build and run, and you’ll see the actual cover for the book you chose from the list!
Sharing the Image
The last thing to do is allow your users to share these cover images. You’ve already seen sharing in action and the code is almost identical here.
First, add another variable to DetailActivity.java:
ShareActionProvider mShareActionProvider; // 14 |
This is just another variable to hold a reference to the ShareActionProvider
for your Activity
.
Next, add this new method to the class:
private void setShareIntent() { // create an Intent with the contents of the TextView Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); shareIntent.putExtra(Intent.EXTRA_SUBJECT, "Book Recommendation!"); shareIntent.putExtra(Intent.EXTRA_TEXT, mImageURL); // Make sure the provider knows // it should work with that Intent mShareActionProvider.setShareIntent(shareIntent); } |
This should look very familiar, as it is nearly the same as the method you added to MainActivity
earlier. The only difference is the use of mImageURL
as the text to be shared.
One final thing left to do – you need to add the share button to the Action Bar. You can again reuse code from MainActivity
. Add this method to DetailActivity.java:
@Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu // this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); // Access the Share Item defined in menu XML MenuItem shareItem = menu.findItem(R.id.menu_item_share); // Access the object responsible for // putting together the sharing submenu if (shareItem != null) { mShareActionProvider = (ShareActionProvider) shareItem.getActionProvider(); } setShareIntent(); return true; } |
Here, you could potentially use different menu XML files to populate the Action Bar on different screens/activities. But for your purposes, the same menu/main.xml file will do.
Build and run your app, and you’ll have a pretty powerful app! Your app now takes in your search query, returns a list of books,allows you to take a closer look at a book cover, and share that cover image with friends!
Where to Go From Here?
Congratulations on putting together your app! This tutorial has introduced you to a variety of Android capabilities and techniques, and I hope you feel comfortable with the basics of development on the Android platform.
You can get the full source code for this app on GitHub or as a .zip.
If you’re looking for a few more challenges, how about trying some of these?
- Add a
contentDescription
attribute to yourImageView
s for accessibility. Here’s an explanation. - Display more details about the book in the Detail View, like more information about the book or even the first few lines of the book.
- Reposition some of the views to a different layout structure.
- Investigate
android:background
and add background colors to your layouts and views. - Add a hashtag to the share text.
Thank you for following this tutorial series. Please leave any comments or questions below!
The Android robot is reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License.
Make Your First Android App: Part 3/3 is a post from: Ray Wenderlich
The post Make Your First Android App: Part 3/3 appeared first on Ray Wenderlich.