This practical codelab is part of Unit 3: Working in the background in the Android Developer Fundamentals (Version 2) course. You will get the most value out of this course if you work through the codelabs in sequence:

Introduction

In this practical you use an AsyncTask to start a background task that gets data from the internet using a simple REST API. You use the Google APIs Explorer to query the Books API, implement this query in a worker thread using an AsyncTask, and display the result in your UI.

Then you reimplement the same background task using AsyncTaskLoader, which is a more efficient way to update your UI.

What you should already know

You should be able to:

What you'll learn

What you'll do

You will build an app that contains an EditText and a Button.

Once the app is working, you modify the app to use AsyncTaskLoader instead of the AsyncTask class.

In this practical you use the Google Books API to search for information about a book, such as the book's author and title. The Books API provides programmatic access to the Google Book Search service using REST APIs. This is the same service used behind the scenes when you manually execute a search on Google Books. You can use the Google APIs Explorer and Google Book Search in your browser to verify that your Android app is getting the expected results.

1.1 Send a Books API Request

  1. Go to the Google APIs Explorer at https://developers.google.com/apis-explorer/.
  2. Click Services in the left nav, and then Books API.
  3. Find books.volumes.list and click that function name. To find within a page, you can press Control+F (Command+F on Mac).

You should see a webpage that lists the parameters of the Books API function that performs book searches.

  1. In the q field, enter a book name or a partial book name, for example "Romeo". The q parameter is the only required field.
  2. In the maxResults field, enter 10 to limit the results to the top 10 matching books.
  3. In the printType field, enter books to limit the results to books that are in print.
  4. Make sure that the Authorize requests using OAuth 2.0 switch at the top of the form is off.
  5. Click Execute without OAuth link at the bottom of the form.
  6. Scroll down to see the HTTP request and HTTP response.

The HTPP request is a uniform resource identifier (URI). A URI is a string that identifies a resource, and a URL is a certain type of URI that identifies a web resource. For the Books API, the request is a URL. The search parameters that you entered into the form follow the ? in the URL.

Notice the API key field at the end of the URL. For security reasons, when you access a public API, you must obtain an API key and include it in your request. The Books API doesn't require an API key, so you can leave out that portion of the request URI in your app.

1.2 Analyze the Books API response

The response to the query is towards the bottom of the page. The response uses the JSON format, which is a common format for API query responses. In the APIs Explorer web page, the JSON code is nicely formatted so that it is human readable. In your app, the JSON response will be returned from the API service as a single string, and you will need to parse that string to extract the information you need.

The response is made up of name/value pairs that are separated by commas. For example, "kind": "books#volumes" is a name/value pair, where "kind" is the name and "books#volumes" is the value. This is the JSON format.

  1. Find the value for the "title" name for one book. Notice that this result contains a single value.
  2. Find the value for the "authors" name for one book. Notice that this result is an array that can contain more than one value.

The book search includes all the books that contain the search string, with multiple objects to represent each book. In this practical, you only return the title and authors of the first item in the response.

Now that you're familiar with the Books API, it's time to set up the layout of your app.

2.1 Create the project and user interface (UI)

  1. Create a new project called "WhoWroteIt", using the Empty Activity template. Accept the defaults for all the other options.
  2. Open the activity_main.xml layout file. Click the Text tab.
  3. Add the layout_margin attribute to the top-level ConstraintLayout:
android:layout_margin="16dp"
  1. Delete the existing TextView.
  2. Add the following UI elements and attributes to the layout file. Note that the string resources will appear in red; you define those in the next step.

View

Attributes

Values

TextView

android:layout_width

android:layout_height

android:id

android:text

android:textAppearance

app:layout_constraintStart_toStartOf

app:layout_constraintTop_toTopOf

"match_parent"

"wrap_content"

"@+id/instructions"

"@string/instructions"

"@style/TextAppearance.
AppCompat.Title"

"parent"

"parent"

EditText

android:layout_width

android:layout_height

android:id

android:layout_marginTop

android:inputType

android:hint

app:layout_constraintEnd_toEndOf

app:layout_constraintStart_toStartOf

app:layout_constraintTop_toBottomOf

"match_parent"

"wrap_content"

"@+id/bookInput"

"8dp"

"text"

"@string/input_hint"

"parent"

"parent"

"@+id/instructions"

Button

android:layout_width

android:layout_height

android:id

android:layout_marginTop

android:text

android:onClick

app:layout_constraintStart_toStartOf

app:layout_constraintTop_toBottomOf

"wrap_content"

"wrap_content"

"@+id/searchButton"

"8dp"

"@string/button_text"

"searchBooks"

"parent"

"@+id/bookInput"

TextView

android:layout_width

android:layout_height

android:id

android:layout_marginTop

android:textAppearance

app:layout_constraintStart_toStartOf

app:layout_constraintTop_toBottomOf

"wrap_content"

"wrap_content"

"@+id/titleText"

"16dp"

"@style/TextAppearance.

AppCompat.Headline"

"parent"

"@+id/searchButton"

TextView

android:layout_width

android:layout_height

android:id

android:layout_marginTop

android:textAppearance

app:layout_constraintStart_toStartOf

app:layout_constraintTop_toBottomOf

"wrap_content"

"wrap_content"

"@+id/authorText"

"8dp"

"@style/TextAppearance.

AppCompat.Headline"

"parent"

"@+id/titleText"

  1. In the strings.xml file, add these string resources:
<string name="instructions">Enter a book name to find out who wrote the book. </string>
<string name="button_text">Search Books</string>
<string name="input_hint">Book Title</string>
  1. The onClick attribute for the button will be highlighted in yellow, because the searchBooks() method is not yet implemented in MainActivity. To create the method stub in MainActivity, place your cursor in the highlighted text, press Alt+Enter (Option+Enter on a Mac) and choose Create 'searchBooks(View) in 'MainActivity'.

Solution code for activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:layout_margin="16dp"
   tools:context=".MainActivity">

   <TextView
       android:id="@+id/instructions"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:text="@string/instructions"
       android:textAppearance="@style/TextAppearance.AppCompat.Title"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent"/>

   <EditText
       android:id="@+id/bookInput"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_marginTop="8dp"
       android:hint="@string/input_hint"
       android:inputType="text"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/instructions"/>

   <Button
       android:id="@+id/searchButton"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_marginTop="8dp"
       android:onClick="searchBooks"
       android:text="@string/button_text"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/bookInput"/>

   <TextView
       android:id="@+id/titleText"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_marginTop="16dp"
       android:textAppearance=
          "@style/TextAppearance.AppCompat.Headline"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/searchButton" />

   <TextView
       android:id="@+id/authorText"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_marginTop="8dp"
       android:textAppearance=
          "@style/TextAppearance.AppCompat.Headline"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/titleText"/>

</android.support.constraint.ConstraintLayout>

2.2 Get user input

To query the Books API, you need to get the user input from the EditText.

  1. In MainActivity.java, create member variables for the EditText, the author TextView, and the title TextView.
private EditText mBookInput;
private TextView mTitleText;
private TextView mAuthorText;
  1. Initialize those variables to views in onCreate().
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);

   mBookInput = (EditText)findViewById(R.id.bookInput);
   mTitleText = (TextView)findViewById(R.id.titleText);
   mAuthorText = (TextView)findViewById(R.id.authorText);
}
  1. In the searchBooks() method, get the text from the EditText view. Convert the text to a String, and assign it to a variable.
public void searchBooks(View view) {
   // Get the search string from the input field.
   String queryString = mBookInput.getText().toString();
}

2.3 Create an empty AsyncTask class

You are now ready to connect to the internet and use the Books API. In this task you create a new AsyncTask subclass called FetchBook to handle connecting to the network.

Network connectivity can be be sluggish, which can make your app erratic or slow. For this reason, don't make network connections on the UI thread. If you attempt a network connection on the UI thread, the Android runtime might raise a NetworkOnMainThreadException to warn you that it's a bad idea.

Instead, use a subclass of AsyncTask to make network connections. An AsyncTask requires three type parameters: an input-parameter type, a progress-indicator type, and a result type.

  1. Create a Java class in your app called FetchBook, that extends AsyncTask. The generic type parameters for the class will be <String, Void, String>. (String because the query is a string, Void because there is no progress indicator, and String because the JSON response is a string.)
public class FetchBook extends AsyncTask<String, Void, String> {

}
  1. Implement the required method, doInBackground(). To do this, place your cursor on the red underlined text, press Alt+Enter (Option+Enter on a Mac) and select Implement methods. Choose doInBackground() and click OK.

Make sure the parameters and return types are correct. (The method takes a variable list of String objects and returns a String.)

@Override
protected String doInBackground(String... strings) {
   return null;
}
  1. Select Code > Override methods, or press Ctrl+O. Select the onPostExecute() method to insert the method definition into the class. The onPostExecute() method takes a String as a parameter and returns void.
@Override
protected void onPostExecute(String s) {
   super.onPostExecute(s);

}
  1. To display the results in the TextView objects in MainActivity, you must have access to those text views inside the AsyncTask. Create WeakReference member variables for references to the two text views that show the results.
private WeakReference<TextView> mTitleText;
private WeakReference<TextView> mAuthorText;
  1. Create a constructor for the FetchBook class that includes the TextView views from MainActivity, and initialize the member variables in that constructor.
FetchBook(TextView titleText, TextView authorText) {
   this.mTitleText = new WeakReference<>(titleText);
   this.mAuthorText = new WeakReference<>(authorText);
}

Solution code for FetchBook:

public class FetchBook extends AsyncTask<String,Void,String> {
   private WeakReference<TextView> mTitleText;
   private WeakReference<TextView> mAuthorText;
        
   public FetchBook(TextView mTitleText, TextView mAuthorText) {
      this.mTitleText = new WeakReference<>(titleText);
      this.mAuthorText = new WeakReference<>(authorText);
   }

   @Override
   protected String doInBackground(String... strings) {
       return null;
   }

   @Override
   protected void onPostExecute(String s) {
       super.onPostExecute(s);
   }
}

2.4 Create the NetworkUtils class and build the URI

You need to open an internet connection and query the Books API. Because you will probably use this functionality again, you may want to create a utility class with this functionality or develop a useful subclass for your own convenience.

In this task, you write the code for connecting to the internet in a helper class called NetworkUtils.

  1. Create a new Java class in your app called NetworkUtils. The NetworkUtils class does not extend from any other class.
  2. For logging, create a LOG_TAG variable with the name of the class:
private static final String LOG_TAG = 
   NetworkUtils.class.getSimpleName();
  1. Create a static method named getBookInfo(). The getBookInfo() method takes the search term as a String parameter and returns the JSON String response from the API you examined earlier.
static String getBookInfo(String queryString){

}
  1. Create the following local variables in the getBookInfo() method. You will need these variables for connecting to the internet, reading the incoming data, and holding the response string.
HttpURLConnection urlConnection = null;
BufferedReader reader = null;
String bookJSONString = null;
  1. At the end of the getBookInfo() method, return the value of bookJSONString.
return bookJSONString;
  1. Add a skeleton try/catch/finally block in getBookInfo(), after the local variables and before the return statement.

In the try block, you'll build the URI and issue the query. In the catch block, you'll handle problems with the request. In the finally block, you'll close the network connection after you finish receiving the JSON data.

try {
   //...
} catch (IOException e) {
      e.printStackTrace();
} finally {
   //...
}
  1. Create the following member constants at the top of the the NetworkUtils class, below the LOG_TAG constant:
// Base URL for Books API.
private static final String BOOK_BASE_URL =  "https://www.googleapis.com/books/v1/volumes?";
// Parameter for the search string.
private static final String QUERY_PARAM = "q";
// Parameter that limits search results.
private static final String MAX_RESULTS = "maxResults";
// Parameter to filter by print type.
private static final String PRINT_TYPE = "printType";

As you saw in the request on the Books API web page, all of the requests begin with the same URI. To specify the type of resource, append query parameters to that base URI. It is common practice to separate all of these query parameters into constants, and combine them using an Uri.Builder so they can be reused for different URIs. The Uri class has a convenient method, Uri.buildUpon(), that returns a URI.Builder that you can use.

For this app, you limit the number and type of results returned to increase the query speed. To restrict the query, you will only look for books that are printed.

  1. In the getBookInfo() method, build your request URI in the try block:
Uri builtURI = Uri.parse(BOOK_BASE_URL).buildUpon()
       .appendQueryParameter(QUERY_PARAM, queryString)
       .appendQueryParameter(MAX_RESULTS, "10")
       .appendQueryParameter(PRINT_TYPE, "books")
       .build(); 
  1. Also inside the try block, convert your URI to a URL object:
URL requestURL = new URL(builtURI.toString());

2.5 Make the request

This API request uses the HttpURLConnection class in combination with an InputStream, BufferedReader, and a StringBuffer to obtain the JSON response from the web. If at any point the process fails and InputStream or StringBuffer are empty, the request returns null, signifying that the query failed.

  1. In the try block of the getBookInfo() method, open the URL connection and make the request:
urlConnection = (HttpURLConnection) requestURL.openConnection();
urlConnection.setRequestMethod("GET");
urlConnection.connect();
  1. Also inside the try block, set up the response from the connection using an InputStream, a BufferedReader and a StringBuilder.
// Get the InputStream.
InputStream inputStream = urlConnection.getInputStream();

// Create a buffered reader from that input stream.
reader = new BufferedReader(new InputStreamReader(inputStream));

// Use a StringBuilder to hold the incoming response.
StringBuilder builder = new StringBuilder();
  1. Read the input line-by-line into the string while there is still input:
String line;
while ((line = reader.readLine()) != null) {
   builder.append(line); 
   // Since it's JSON, adding a newline isn't necessary (it won't
   // affect parsing) but it does make debugging a *lot* easier
   // if you print out the completed buffer for debugging.
   builder.append("\n");
}
  1. At the end of the input, check the string to see if there is existing response content. Return null if the response is empty.
if (builder.length() == 0) {
   // Stream was empty. No point in parsing.
   return null;
}
  1. Convert the StringBuilder object to a String and store it in the bookJSONString variable.
bookJSONString = builder.toString();
  1. In the finally block, close both the connection and the BufferedReader:
finally {
   if (urlConnection != null) {
       urlConnection.disconnect();
   }
   if (reader != null) {
       try {
           reader.close();
       } catch (IOException e) {
           e.printStackTrace();
       }
   }
}
  1. Just before the final return, print the value of the bookJSONString variable to the log.
Log.d(LOG_TAG, bookJSONString);
  1. In FetchBook, modify the doInBackground() method to call the NetworkUtils.getBookInfo() method, passing in the search term that you obtained from the params argument passed in by the system. (The search term is the first value in the strings array.) Return the result of this method. (This line replaces the null return.)
return NetworkUtils.getBookInfo(strings[0]);
  1. In MainActivity, add this line to the end of the searchBooks() method to launch the background task with the execute() method and the query string.
    new FetchBook(mTitleText, mAuthorText).execute(queryString);
  1. Run your app and execute a search. Your app will crash. In Android Studio, click Logcat to view the logs and see what is causing the error. You should see the following line:
Caused by: java.lang.SecurityException: Permission denied (missing INTERNET permission?)

This error indicates that you have not included the permission to access the internet in your Android manifest. Connecting to the internet introduces security concerns, which is why apps do not have connectivity by default. In the next task you add internet permissions to the manifest.

2.6 Add internet permissions

  1. Open the AndroidManifest.xml file.
  2. Add the following code just before the <application> element:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission 
       android:name="android.permission.ACCESS_NETWORK_STATE" />
  1. Build and run your app again. In Android Studio, click Logcat to view the log. Note that this time, the query runs correctly and the JSON string result is printed to the log.

2.7 Parse the JSON string

Now that you have a JSON response to your query, you must parse the results to extract the information you want to display in your app's UI. Java has classes in its core API help you parse and handle JSON-type data. This process, as well as updating the UI, happen in the onPostExecute() method of your FetchBook class.

There is a chance that the doInBackground() method won't return the expected JSON string. For example, the try/catch might fail and throw an exception, the network might time out, or other unhandled errors might occur. In those cases, the JSON parsing will fail and will throw an exception. To handle this case, do the JSON parsing in a try/catch block, and handle the case where incorrect or incomplete data is returned.

  1. In the FetchBook class, in the onPostExecute() method, add a try/catch block below the call to super.
try {
   //...
} catch (JSONException e) {
      e.printStackTrace();
}
  1. Inside the try block, use the classes JSONObject and JSONArray to obtain the JSON array of items from the result string.
JSONObject jsonObject = new JSONObject(s); 
JSONArray itemsArray = jsonObject.getJSONArray("items");
  1. Initialize the variables used for the parsing loop.
int i = 0;
String title = null;
String authors = null;
  1. Iterate through the itemsArray array, checking each book for title and author information. With each loop, test to see if both an author and a title are found, and if so, exit the loop. This way, only entries with both a title and author will be displayed.
while (i < itemsArray.length() && 
   (authors == null && title == null)) {
    // Get the current item information.
    JSONObject book = itemsArray.getJSONObject(i);
    JSONObject volumeInfo = book.getJSONObject("volumeInfo");

    // Try to get the author and title from the current item,
    // catch if either field is empty and move on.
    try {
        title = volumeInfo.getString("title");
        authors = volumeInfo.getString("authors");
    } catch (Exception e) {
        e.printStackTrace();
    }

    // Move to the next item.
    i++;
}
  1. If a matching response is found, update the UI with that response. Because the references to the TextView objects are WeakReference objects, you have to dereference them using the get() method.
if (title != null && authors != null) {
    mTitleText.get().setText(title);
    mAuthorText.get().setText(authors);
}
  1. If the loop has stopped and the result has no items with both a valid author and a valid title, set the title TextView to a "no results" string resource and clear the author TextView.
} else {
    mTitleText.get().setText(R.string.no_results);
    mAuthorText.get().setText("");
}
  1. In the catch block, print the error to the log. Set the title TextView to the "no results" string resource, and clear the author TextView.
} catch (Exception e) {
   // If onPostExecute does not receive a proper JSON string,
   // update the UI to show failed results.
   mTitleText.get().setText(R.string.no_results);
   mAuthorText.get().setText("");
   e.printStackTrace();
}
  1. Add the no_results resource to strings.xml:
<string name="no_results">"No Results Found"</string>

Solution code:

@Override
protected void onPostExecute(String s) {
   super.onPostExecute(s);

   try {
       // Convert the response into a JSON object.
       JSONObject jsonObject = new JSONObject(s);
       // Get the JSONArray of book items.
       JSONArray itemsArray = jsonObject.getJSONArray("items");

       // Initialize iterator and results fields.
       int i = 0;
       String title = null;
       String authors = null;

       // Look for results in the items array, exiting 
       // when both the title and author
       // are found or when all items have been checked.
       while (i < itemsArray.length() && 
          (authors == null && title == null)) {
           // Get the current item information.
           JSONObject book = itemsArray.getJSONObject(i);
           JSONObject volumeInfo = book.getJSONObject("volumeInfo");

           // Try to get the author and title from the current item,
           // catch if either field is empty and move on.
           try {
               title = volumeInfo.getString("title");
               authors = volumeInfo.getString("authors");
           } catch (Exception e) {
               e.printStackTrace();
           }

           // Move to the next item.
           i++;
       }

       // If both are found, display the result.
       if (title != null && authors != null) {
           mTitleText.get().setText(title);
           mAuthorText.get().setText(authors);
       } else {
           // If none are found, update the UI to 
           // show failed results.
           mTitleText.get().setText(R.string.no_results);
           mAuthorText.get().setText("");
       }

   } catch (Exception e) {
       // If onPostExecute does not receive a proper JSON string,
       // update the UI to show failed results.
       mTitleText.get().setText(R.string.no_results);
       mAuthorText.get().setText("");
   }
}

You now have a functioning app that uses the Books API to execute a book search. However, a few things do not behave as expected:

You fix the first two of these issues in this section, and the last issue in Task 4.

3.1 Hide the keyboard and update the TextView

The user experience of searching is not intuitive. When the user taps the button, the keyboard remains visible, and the user has no way of knowing that the query is in progress.

One solution is to programmatically hide the keyboard and update one of the result text views to read "Loading..." while the query is performed.

  1. In MainActivity, add the following code to the searchBooks() method, after the queryString definition. The code hides the keyboard when the user taps the button.
InputMethodManager inputManager = (InputMethodManager)
   getSystemService(Context.INPUT_METHOD_SERVICE);

if (inputManager != null ) {
   inputManager.hideSoftInputFromWindow(view.getWindowToken(),
           InputMethodManager.HIDE_NOT_ALWAYS);
}
  1. Just beneath the call to execute the FetchBook task, add code to change the title TextView to a loading message and clear the author TextView.
new FetchBook(mTitleText, mAuthorText).execute(queryString);
mAuthorText.setText("");
mTitleText.setText(R.string.loading);
  1. Add the loading resource to strings.xml:
<string name="loading">Loading...</string>

3.2 Manage the network state and the empty search field case

Whenever your app uses the network, it needs to handle the possibility that a network connection is unavailable. Before attempting to connect to the network, your app should check the state of the network connection. In addition, it should not try to query the Books API if the user has not entered a query string.

  1. In the searchBooks() method, use the ConnectivityManager and NetworkInfo classes to check the network connection. Add the following code after the input manager code that hides the keyboard:
ConnectivityManager connMgr = (ConnectivityManager)
           getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = null;
if (connMgr != null) {
   networkInfo = connMgr.getActiveNetworkInfo();
}
  1. Add a test around the call to the FetchBook task and TextView updates to ensure that the network connection exists, that the network is connected, and that a query string is available.
if (networkInfo != null && networkInfo.isConnected()
           && queryString.length() != 0) {
   new FetchBook(mTitleText, mAuthorText).execute(queryString);
   mAuthorText.setText("");
   mTitleText.setText(R.string.loading);
}
  1. Add an else block to that test. In the else block, update the UI with a no_search_term error message if there is no term to search for, and a no_network error message otherwise.
} else {
   if (queryString.length() == 0) {
       mAuthorText.setText("");
       mTitleText.setText(R.string.no_search_term);
   } else {
       mAuthorText.setText("");
       mTitleText.setText(R.string.no_network);
   }
}
  1. Add the no_search_term and no_network resources to strings.xml:
<string name="no_search_term">Please enter a search term</string>
<string name="no_network">Please check your network connection and try again.</string>

Solution code:

public void searchBooks(View view) {
   String queryString = mBookInput.getText().toString();

   InputMethodManager inputManager = (InputMethodManager)
           getSystemService(Context.INPUT_METHOD_SERVICE);
   if (inputManager != null ) {
       inputManager.hideSoftInputFromWindow(view.getWindowToken(),
               InputMethodManager.HIDE_NOT_ALWAYS);
   }

   ConnectivityManager connMgr = (ConnectivityManager)
           getSystemService(Context.CONNECTIVITY_SERVICE);
   NetworkInfo networkInfo = null;
   if (connMgr != null) {
       networkInfo = connMgr.getActiveNetworkInfo();
   }

   if (networkInfo != null && networkInfo.isConnected()
           && queryString.length() != 0) {
       new FetchBook(mTitleText, mAuthorText).execute(queryString);
       mAuthorText.setText("");
       mTitleText.setText(R.string.loading);
   } else {
       if (queryString.length() == 0) {
           mAuthorText.setText("");
           mTitleText.setText(R.string.no_search_term);
       } else {
           mAuthorText.setText("");
           mTitleText.setText(R.string.no_network);
       }
   }
}

Solution code

The solution code for this practical up to this point is in the Android Studio project WhoWroteIt.

When you use an AsyncTask to perform operations in the background, that background thread can't update the UI if a configuration change occurs while the background task is running. To address this situation, use the AsyncTaskLoader class.

AsyncTaskLoader loads data in the background and reassociates background tasks with the Activity, even after a configuration change. With an AsyncTaskLoader, if you rotate the device while the task is running, the results are still displayed correctly in the Activity.

Why use an AsyncTask if an AsyncTaskLoader is much more useful? The answer is that it depends on the situation. If the background task is likely to finish before any configuration changes occur, and it's not crucial for the task to update the UI, an AsyncTask may be sufficient. The AsyncTaskLoader class actually uses an AsyncTask behind the scenes to work its magic.

In this exercise you learn how to use AsyncTaskLoader instead of AsyncTask to run your Books API query.

4.1 Create an AsyncTaskLoader class

  1. To preserve the results of the previous practical, copy the WhoWroteIt project. Rename the copied project "WhoWroteItLoader".
  2. Create a class called BookLoader that extends AsyncTaskLoader with parameterized type <String>.
import android.support.v4.content.AsyncTaskLoader;

public class BookLoader extends AsyncTaskLoader<String> {
  
}

Make sure to import the AsyncTaskLoader class from the v4 Support Library.

  1. Implement the required loadInBackground() method. Notice the similarity between this method and the initial doInBackground() method from AsyncTask.
@Nullable
@Override
public String loadInBackground() {
   return null;
}
  1. Create the constructor for the BookLoader class. With your text cursor on the class declaration line, press Alt+Enter (Option+Enter on a Mac) and select Create constructor matching super. This creates a constructor with the Context as a parameter.
public BookLoader(@NonNull Context context) {
   super(context);
}

4.2 Implement required methods

  1. Press Ctrl+O to open the Override methods menu, and select onStartLoading. The system calls this method when you start the loader.
@Override
protected void onStartLoading() {
   super.onStartLoading();
}
  1. Inside the onStartLoading() method stub, call forceLoad() to start the loadInBackground() method. The loader will not start loading data until you call the forceLoad() method.
@Override
protected void onStartLoading() {
   super.onStartLoading();
}
  1. Create a member variable called mQueryString to hold the string for the Books API query. Modify the constructor to take a String as an argument and assign it to the mQueryString variable.
private String mQueryString;

BookLoader(Context context, String queryString) {
   super(context);
   mQueryString = queryString;
}
  1. In the loadInBackground() method, replace the return statement with the following code, which calls the NetworkUtils.getBookInfo() method with the query string and returns the result:
return NetworkUtils.getBookInfo(mQueryString);

4.3 Modify MainActivity

The connection between the AsyncTaskLoader and the Activity that calls it is implemented with the LoaderManager.LoaderCallbacks interface. These loader callbacks are a set of methods in the activity that are called by the LoaderManager when the loader is being created, when the data has finished loading, and when the loader is reset. The loader callbacks take the results of the task and pass them back to the activity's UI.

In this task you implement the LoaderManager. LoaderCallbacks interface in your MainActivity to handle the results of the loadInBackground() AsyncTaskLoader method.

  1. In MainActivity, add the LoaderManager.LoaderCallbacks implementation to the class declaration, parameterized with the String type:
public class MainActivity extends AppCompatActivity  
   implements LoaderManager.LoaderCallbacks<String> { 

Make sure to import the LoaderManager.LoaderCallbacks class from the v4 Support Library.

  1. Implement all the required callback methods from the interface. Thi includes onCreateLoader(), onLoadFinished(), and onLoaderReset(). Place your cursor on the class signature line and press Alt+Enter (Option+Enter on a Mac). Make sure that all the methods are selected and click OK.
@NonNull
@Override
public Loader<String> onCreateLoader(int id, @Nullable Bundle args) {
   return null;
}

@Override
public void onLoadFinished(@NonNull Loader<String> loader, String data) {

}

@Override
public void onLoaderReset(@NonNull Loader<String> loader) {

}

About the required methods:

For this app, you only implement the first two methods. Leave onLoaderReset() empty.

  1. The searchBooks() method is the onClick method for the button. In searchBooks(), replace the call to execute the FetchBook task with a call to restartLoader(). Pass in the query string that you got from the EditText in the loader's Bundle object:
Bundle queryBundle = new Bundle();
queryBundle.putString("queryString", queryString);
getSupportLoaderManager().restartLoader(0, queryBundle, this);

The restartLoader() method is defined by the LoaderManager, which manages all the loaders used in an activity or fragment. Each activity has exactly one LoaderManager instance that is responsible for the lifecycle of the Loaders that the activity manages.

The restartLoader() method takes three arguments:

4.4 Implement loader callbacks

In this task you implement the onCreateLoader() and onLoadFinished() callback methods to handle the background task.

  1. In onCreateLoader(), replace the return statement with a statement that returns an instance of the BookLoader class. Pass in the context (this) and the queryString obtained from the passed-in Bundle:
@NonNull
@Override
public Loader onCreateLoader(int id, @Nullable Bundle args) {
   String queryString = "";

   if (args != null) {
       queryString = args.getString("queryString");
   }

   return new BookLoader(this, queryString);
}
  1. Copy the code from onPostExecute() in your FetchBook class to onLoadFinished() in your MainActivity. Remove the call to super.onPostExecute(). This is the code that parses the JSON result for a match with the query string.
  2. Remove all the calls to get() for each of the TextView objects. Because updating the UI happens in the Activity itself, you no longer need weak references to the original views.
  3. Replace the argument to the JSONObject constructor (the variable s) with the parameter data.
JSONObject jsonObject = new JSONObject(data);
  1. Run your app. You should have the same functionality as before, but now in a loader! However, when you rotate the device, the view data is lost. That's because when the activity is created (or recreated), the activity doesn't know that a loader is running. To reconnect to the loader, you need an initLoader() method in the onCreate() of MainActivity.
  2. Add the following code in onCreate() to reconnect to the loader, if the loader already exists:
if(getSupportLoaderManager().getLoader(0)!=null){
   getSupportLoaderManager().initLoader(0,null,this);
}

If the loader exists, initialize it. You only want to reassociate the loader to the activity if a query has already been executed. In the initial state of the app, no data is loaded, so there is no data to preserve.

  1. Run your app again and rotate the device. The loader manager now holds onto your data across device-configuration changes!
  2. Remove the FetchBook class, because it is no longer used.

Solution code

The solution code for this task is in the Android Studio project WhoWroteItLoader.

Challenge: Explore the the Books API in greater detail and find a search parameter that restricts the results to books that are downloadable in the EPUB format. Add the parameter to your request and view the results.

<uses-permission android:name="android.permission.INTERNET" />

The AsyncTask class lets you run tasks in the background instead of on the UI thread:

When an AsyncTask executes, it goes through four steps:

  1. onPreExecute() runs on the UI thread before the task is executed. This step is normally used to set up the task, for instance by showing a progress bar in the UI.
  2. doInBackground(Params...) runs on the background thread immediately after onPreExecute() finishes. This step performs background computations that can take a long time.
  3. onProgressUpdate(Progress...) runs on the UI thread after you a call publishProgress(Progress...).
  4. onPostExecute(Result) runs on the UI thread after the background computation is finished. The result of the computation is passed to onPostExecute().

AsyncTaskLoader is the loader equivalent of an AsyncTask.

The related concept documentation is in 7.2: Internet connection.

Android developer documentation:

This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:

Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.

If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.

Build and run an app

Create an app that retrieves and displays the contents of a web page that's located at a URL. The app displays the following:

Use an AsyncTaskLoader to retrieve the source code of the web page at the URL. You need to implement a subclass of AsyncTaskLoader.

If connection to the internet is not available when the user taps the button, the app must show the user an appropriate response. For example, the app might display a message such as "Check your internet connection and try again."

The display must contain a TextView in a ScrollView that displays the source code, but the exact appearance of the interface is up to you. Your screen can look different from the screenshots below. You can use a pop-up menu, spinner, or checkboxes to allow the user to select HTTP or HTTPS.

The image on the left shows the starting screen, with a pop-up menu for the protocol. The image on the right shows an example of the results of retrieving the page source for given URL.

Answer these questions

Question 1

What permissions does your app need to connect to the internet?

Question 2

How does your app check that internet connectivity is available?

In the manifest:

In the code:

Question 3

Where do you implement the loader callback method that's triggered when the loader finishes executing its task?

Question 4

When the user rotates the device, how do AsyncTask and AsyncTaskLoader behave differently if they are in the process of running a task in the background?

Question 5

How do you initialize an AsyncTaskLoader to perform steps, such as initializing variables, that must be done before the loader starts performing its background task?

Question 6

What methods must an AsyncTaskLoader implement?

Submit your app for grading

Guidance for graders

Check that the app has the following features:

To find the next practical codelab in the Android Developer Fundamentals (V2) course, see Codelabs for Android Developer Fundamentals (V2).

For an overview of the course, including links to the concept chapters, apps, and slides, see Android Developer Fundamentals (Version 2).