This content has been archived.

Archive

Older content from our site that still has value for lots of developers!

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!

Version

  • Other, Other, Other

android_tutorial_title_image

Note: We have a more up-to-date version of this tutorial here. We recommend you check that out instead. Enjoy!

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. The final version of the source from Part 2 is also available on GitHub or as a .zip.

Tapping the share icon on the Action Bar reveals a lot of choices!

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.

adding_ic_files

Your src/main/res/drawable-hdpi directory should now look something like this:

image_files_added

Note: When importing your images you may receive an error saying “Refactoring cannot be performed when importing images”. If that is the case try to drag your images into the project again whilst holding the Alt key to resolve it.

Only the hdpi assets are provided here. For future projects, it is a good idea to add assets for the other dpi values as well.

If you have trouble finding the hdpi folder in the drawables folder. Studio may be defaulting to it’s “Android” project view structure. Change this to “Project” by clicking on the large button to the left just above the project hierarchy on the left. Once you have imported the images you can change back if you prefer

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, your device or emulator’s home screen should reflect the name and icon updates. For example:

new_icon_on_home_screen

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!

You will also find Android will ask your user if they would like to quit an unresponsive App, which isn’t great for your App if people are scrambling to leave it as quick as they can.

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.

Note: The old-fashioned way to incorporate third-party libraries into your code was to download a zipped-up .jar file, copy it into your code and then include it in your project’s build path. It’s not too difficult, but the larger inconvenience is what to do when the library updates, or if you need to share your project with teammates.

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 build.gradle. Note that there are two build.gradle files in your project. You want the one that’s within the app folder – not the one at the project root level:

build_gradle_file

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: 'com.android.application').

Scroll down to the area labeled dependencies. 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:

sync_gradle

Note: If you get an error that says “requires compiling with JDK 7”, you should upgrade your SDK to JDK 7. You can download it here, then follow the prompts to set your SDK folder to /Library/Java/JavaVirtualMachines/jdk1.7.0_71.jdk/Contents/Home.

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. You can see this in your root folder’s build.gradle file, it looks like this.

repositories {
        jcenter()
    }

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.

Note: jcenter() is a Gradle method that connects to a repository which is a superset of the Maven Central Repository. In early versions of Android Studio the default repository set by Gradle was Maven Central, you still change it back by changing jcenter() to mavenCentral().

jcenter however is thought of as being faster and more responsive whilst still providing all the libraries available on the Maven Central Repository. So lets leave this alone.

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 to your growing list 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 anywhere in your MainActivity.java Class:

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. This parameter will be made up of base URL defined at the top of your class, plus the search string you will add later.
  • 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 called onSuccess and onFailure 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 everything inside the Method 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!

Note: If Studio runs into any issues with its Gradle sync, you may get a 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. You will still see something happening though, open LogCat and you can see the resulting JSON spilled out whenever the API call finishes.

query_in_logcat

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:

cell_sample

Right-click on the res/layout folder in the Studio left pane, and select New > Layout resource file.

new_layout_res_file

Name your file row_book.xml with a root element of RelativeLayout, then click OK.

new_xml_file

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.

Note: One quick thing before moving on. Attributes for children of RelativeLayouts often reference other children using IDs, as you saw just now. 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 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, make sure the Kind is set to Class and then hit OK.

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. Your old friend Context. Remember Context is the object that lets other objects know what is happening in your App. Here Context is going to help Picasso, the image downloader Library, display the images you download in your App
  • 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 the 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 stored in a JSONArray. So all you have to do is complete a lookup on 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.

Note: An example URL from this operation: http://covers.openlibrary.org/b/id/6845816-S.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!

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.

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!

first_query_w_list

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.” Lets solve that now.

Add the following line to the list of variables at the top of MainActivity.java:

ProgressDialog mDialog;

ProgressDialog is an Android class that provides a convienent solution for times when you want to provide feedback that your App is performing some sort of intensive task. That could be fetching large files and reading their contents, setting up basic values for the first time the App run or fetching online content like your current App does.

Lets set some initial values, go to your onCreate Method and add these lines at the end:

mDialog = new ProgressDialog(this);
mDialog.setMessage("Searching for Book");
mDialog.setCancelable(false);

Good. You’ve told your Activity you want a ProgressDialog to be created with a particular message and that you don’t want the user to have the ability to cancel it. Lets put this to work, add this line to queryBooks, immediately after creating your AsyncHttpClient:

// Show ProgressDialog to inform user that a task in the background is occurring
mDialog.show();

You want the Dialog to disappear 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. Dismiss the ProgressDialog
mDialog.dismiss();

Then add the same line to the beginning of onFailure:

// 11. Dismiss the ProgressDialog
mDialog.dismiss();

Run the app again and do another search.

This time, your Dialog will appear in the middle of your screen and will spin as your networking call is happening. Much better! Your App users now know when something is happening behind the scenes thanks to you.

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.

new_layout_resource_file

Name it activity_detail.xml, with a Root Element of ImageView.

activity_detail

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 in Text mode 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.

making_detail_activity

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 ActionBarActivity {

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
    getSupportActionBar().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 getSupportActionBar 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=".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 recall from earlier in this tutorial 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.

Note: I put in a 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.

detail_activity_placeholder

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, the Picasso Library will 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!

book_cover_image

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 the top of DetailActivity.java:

ShareActionProvider mShareActionProvider; // 14

This is just another variable to hold a reference to the ShareActionProvider for your Activity. Make sure you import the right ShareActionProvider. It should match the import in your MainActivity.java class.

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 added to MainActivity earlier. The only difference is the use of mImageURL as the text to be shared.

Note: Some share service providers, like Facebook, will intelligently interpret the URL as an image and display it to the user’s friends. Others, like e-mail, will simply include the link. Either way, you’re enabling your users to share dynamic content about a book they think others might enjoy!

One final thing left to do – adding 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.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) MenuItemCompat.getActionProvider(shareItem);
    }

    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/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!

sharing_photo

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 your ImageViews 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.

Add a rating for this content

Contributors

Comments