7.2: Connect to the Internet with AsyncTask and AsyncTaskLoader

Contents:

In this practical you will use an AsyncTask to start a background task which gets data from the Internet using a simple REST API. You will use the Google API Explorer to learn how to query the Book Search API, implement this query in a worker thread using AsyncTask, and display the result in your UI. Then you will reimplement the same background task using AsyncTaskLoader, which will be more efficient in updating your UI, handling performance issues, and improving the overall UX.

What you should already KNOW

From the previous practicals you should be able to:

  • Create an activity.
  • Add a TextView to the layout for the activity.
  • Implement onClick functionality to a button in your layout.
  • Implement an AsyncTask and display the result in your UI.
  • Pass information between activities as extras.

What you will LEARN

In this practical, you will learn to:

  • Use the Google API Explorer to investigate Google APIs and to view JSON responses to http requests.
  • Use the Books API as an example API retrieving data over the Internet and keep the UI fast and responsive. You won't learn the Books API in detail in this practical. Your app will only use the simple book search function. To learn more about the Books API see the Books API reference documentation.
  • Parse the JSON results returned from your API query.
  • Implement an AsyncTaskLoader that preserves data upon configuration changes.
  • Update your UI using the Loader callbacks.

What you will DO

In this practical, you will:

  • Use the Google API Explorer to learn about the simple search feature of the Books API.
  • Create the "Who Wrote It?" application that queries the Books API using a worker thread and displays the result in the UI.
  • Modify the "Who Wrote it?" app to use an AsyncTaskLoader instead of an AsyncTask.

App Overview

You will build an app that contains an EditText field and a Button. The user enters the name of the book in the EditText field and clicks the button. The button executes an AsyncTask which queries the Google Book Search API to find the author and title of the book the user is looking for. The results are retrieved and displayed in a TextView field below the button. Once the app is working, you will then modify the app to use AsyncTaskLoader instead of the AsyncTask class.

Preview for the Who Wrote It? app

Task 1. Explore the Books API

In this practical you will use the Google Books API to search for information about a book, such as the author(s) and the title. The Google 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 API 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 (found at https://developers.google.com/apis-explorer/).
  2. Click Books API.
  3. Find (Ctrl-F or Cmd-F) books.volumes.list and click that function name. You should see a webpage that lists the various parameters of the Books API function that performs the book searches.
  4. In the q field enter a book name, or partial book name. The q parameter is the only required field.
  5. Use the maxResults and printType fields to limit the results to the top 10 matching books that were printed. The maxResults field takes an integer value that limits the amount of results per query. The printType field takes one of three string arguments: all, which does not limit the results by type at all; books, which returns only books in print; and magazines which returns only magazines.
  6. Make sure that the "Authorize requests using OAuth 2.0" switch at the top of the form is turned off. Click Execute without OAuth at the bottom of the form.
  7. Scroll down to see the Request and Response.

The Request field is an example of a Uniform Resource Identifier (URI). A URI is a string that names or locates a particular resource. URLs are a certain type of URI for identifying and locating a web resource. For the Books API, the request is a URL that contains your search as a parameter (following the q parameter). Notice the API key field after the query field. For security reasons, when accessing a public API, you usually need to get an API key and include it in your Request. However, this specific API does not require a key, so you can leave out that portion of the Request URI in your app.

1.2 Analyze the Books API Response

Towards the bottom of the page you can see the Response to the query. The response uses the JSON format, which is a common format for API query responses. In the API Explorer web page, the JSON code is nicely formatted so that it is human readable. In your application, 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.

  1. In the Response section, find the value for the "title" key. Notice that this result has a single key and value.
  2. Find the value for the "authors" key. Notice that this one can contain an array of values.
  3. In this practical, you will only return the title and authors of the first item.

Task 2. Create the "Who Wrote It?" App

Now that you are familiar with the Books API method that you will be using, it's time to set up the layout of your application.

2.1 Create the project and user interface

  1. Create an app project called Who Wrote It? with one Activity, using the Empty Activity Template.
  2. Add the following UI elements in the XML file, using a vertical LinearLayout as root view—the view that contains all the other views inside a layout XML file. Make sure the LinearLayout uses android:orientation="vertical":

    View

    Attributes

    Values

    TextView

    android:layout_width

    android:layout_height

    android:id

    android:text

    android:textAppearance

    wrap_content

    wrap_content

    @+id/instructions

    @string/instructions

    @style/TextAppearance.AppCompat.Title

    EditText

    android:layout_width

    android:layout_height

    android:id

    android:inputType

    android:hint

    match_parent

    wrap_content

    @+id/bookInput

    text

    @string/input_hint

    Button

    android:layout_width

    android:layout_height

    android:id

    android:text

    android:onClick

    wrap_content

    wrap_content

    @+id/searchButton

    @string/button_text

    searchBooks

    TextView

    android:layout_width

    android:layout_height

    android:id

    android:textAppearance

    wrap_content

    wrap_content

    @+id/titleText

    @style/TextAppearance.AppCompat.Headline

    TextView

    android:layout_width

    android:layout_height

    android:id

    android:textAppearance

    wrap_content

    wrap_content

    @+id/authorText

    @style/TextAppearance.AppCompat.Headline

  3. In the strings.xml file, add these string resources:
    <string name="instructions">Enter a book name, or part of a
    book name, or just some text from a book to find
    the full book title and who wrote the book!</string>
    <string name="button_text">Search Books</string>
    <string name="input_hint">Enter a Book Title</string>
    
  4. Create a method called searchBooks() in MainActivity.java to handle the onClick button action. As with all onClick methods, this one takes a View as a parameter.

2.2 Set up the Main Activity

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.
  2. Initialize these variables in onCreate().
  3. In the searchBooks() method, get the text from the EditText widget and convert to a String, assigning it to a string variable.
    String queryString = mBookInput.getText().toString();
    
Note: mBookInput.getText() returns an "Editable" datatype which needs to be converted into a string.

2.3 Create an empty AsyncTask

You are now ready to connect to the Internet and use the Book Search REST API. Network connectivity can be sometimes be sluggish or experience delays. This may cause your app to behave erratically or become slow, so you should not make a network connection on the UI thread. If you attempt a network connection on the UI thread, the Android Runtime may raise a NetworkOnMainThreadException to warn you that it's a bad idea.

Use an AsyncTask to make network connections:

  1. Create a new Java class called FetchBook in app/java that extends AsyncTask. An AsyncTask requires three arguments:

    • The input parameters.
    • The progress indicator.
    • The result type.

    The generic type parameters for the task will be <String, Void, String>since the AsyncTask takes a String as the first parameter (the query), Void since there is no progress update, and String since it returns a string as a result (the JSON response).

  2. Implement the required method, doInBackground(), by placing your cursor on the red underlined text, pressing Alt + Enter (Opt + Enter on a Mac) and selecting Implement methods. Choose doInBackground() and click OK. Make sure the parameters and return types are the correct type (It takes a String array and returns a String).
    1. Click the Code menu and choose Override methods (or press Ctrl + O). Select the onPostExecute() method. The onPostExecute() method takes a String as a parameter and returns void.
  3. To display the results in the TextViews, you must have access to those TextViews inside the AsyncTask. Create member variables in the FetchBook AsyncTask for the two TextViews that show the results, and initialize them in a constructor. You will use this constructor in your MainActivity to pass along the TextViews to your AsyncTask.

Solution code for FetchBook:

public class FetchBook extends AsyncTask<String,Void,String>{
        private TextView mTitleText;
        private TextView mAuthorText;

   public FetchBook(TextView mTitleText, TextView mAuthorText) {
       this.mTitleText = mTitleText;
       this.mAuthorText = mAuthorText;
   }

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

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

2.4 Create the NetworkUtils class and build the URI

In this step, you will open an Internet connection and query the Books API. This section has quite a lot of code, so remember to visit the developer documentation for Connecting to the Network if you get stuck. You will write the code for connecting to the internet in a helper class called NetworkUtils.

  1. Create a new Java class called NetworkUtils by clicking File > New > Java Class and only filling in the "Name" field.
  2. Create a unique LOG_TAG variable to use throughout NetworkUtils class for logging:
    private static final String LOG_TAG = NetworkUtils.class.getSimpleName();
    
  3. Create a new static method called getBookInfo() that takes a String as a parameter (which will be the search term) and returns a String (the JSON String response from the API you examined earlier).
    static String getBookInfo(String queryString){}
    
  4. Create the following two local variables in getBookInfo() that will be needed later to help connect and read the incoming data.
    HttpURLConnection urlConnection = null;
    BufferedReader reader = null;
    
  5. Create another local variable at the end of getBookInfo() to contain the raw response from the query and return it:

    String bookJSONString = null;
    return bookJSONString;
    

    If you remember the request from the Books API webpage, you will notice that all the requests begin with the same URI. To specify the type of resource, you append query parameters to the base URI. It is common practice to separate all of these query parameters into constants, and combine them using a Uri.Builder so they can be reused for different URI's. The Uri class has a convenient method, Uri.buildUpon() that returns a URI.Builder that we can use.

    For this application, you will 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.

  6. Create the following member constants in the NetworkUtils class:
    private static final String BOOK_BASE_URL =  "https://www.googleapis.com/books/v1/volumes?"; // Base URI for the Books API
    private static final String QUERY_PARAM = "q"; // Parameter for the search string
    private static final String MAX_RESULTS = "maxResults"; // Parameter that limits search results
    private static final String PRINT_TYPE = "printType";   // Parameter to filter by print type
    
  7. Create a skeleton try/catch/finally block ingetBookInfo(). This is where you will make your HTTP request. The code to build the URI and issue the query will go in the try block. The catch block is used to handle any problems with making the HTTP request and the finally block is for closing the network connection after you've finished receiving the JSON data and returning the result.
    try {
    ...
    } catch (Exception ex) {
    ...
    } finally {
       return bookJSONString;
    }
    
  8. Build up your request URI in the try block:
    //Build up your query URI, limiting results to 10 items and printed books
    Uri builtURI = Uri.parse(BOOK_BASE_URL).buildUpon()
           .appendQueryParameter(QUERY_PARAM, queryString)
           .appendQueryParameter(MAX_RESULTS, "10")
           .appendQueryParameter(PRINT_TYPE, "books")
           .build();
    
  9. Convert your URI to a URL:
    URL requestURL = new URL(builtURI.toString());
    

2.5 Make the Request

It is fairly common to make an API request via the internet. Since 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. This API request uses the HttpURLConnection class in combination with an InputStream and a StringBuffer to obtain the JSON response from the web. If at any point the process fails and InputStream or StringBuffer are empty, it 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();
    
  2. Read the response using an InputStream and a StringBuffer, then convert it to a String:
    InputStream inputStream = urlConnection.getInputStream();
    StringBuffer buffer = new StringBuffer();
    if (inputStream == null) {
       // Nothing to do.
       return null;
    }
    reader = new BufferedReader(new InputStreamReader(inputStream));
    String line;
    while ((line = reader.readLine()) != null) {
       /* 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. */
       buffer.append(line + "\n");
    }
    if (buffer.length() == 0) {
       // Stream was empty.  No point in parsing.
       return null;
    }
    bookJSONString = buffer.toString();
    
  3. Close the try block and log the exception in the catch block.
    catch (IOException e) {
       e.printStackTrace();
       return null;
    }
    
  4. Close both the urlConnection and the reader variables in the finally block:

    finally {
       if (urlConnection != null) {
           urlConnection.disconnect();
       }
       if (reader != null) {
           try {
               reader.close();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
    }
    
    Note: Each time the connection fails, this code returns null. This means that onPostExecute() will have to check its input parameter for a null string and let the user know the connection failed. This error handling strategy is simplistic, as the user has no idea why the connection failed. A better solution for a production application would be to handle each point of failure differently so that the user can get the appropriate feedback.
  5. Log the value of the bookJSONString variable before returning it. You are now done with the getBookInfo() method.

     Log.d(LOG_TAG, bookJSONString);
    
  6. In your AsyncTask doInBackground() method, call the getBookInfo() method, passing in the search term which you obtained from the params argument passed in by the system (it is the first value in the params array). Return the result of this method in the doInBackground() method:
    return NetworkUtils.getBookInfo(params[0]);
    
  7. Now that your AsyncTask is set up, you need to launch it from the MainActivity using the execute() method. Add the following code to your searchBooks() method in MainActivity.java to launch the AsyncTask:
    new FetchBook(mTitleText, mAuthorText).execute(mQueryString);
    
  8. Run your app. Execute a search. Your app will crash. Look at your Logs to 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 AndroidManifest.xml file. Connecting to the internet introduces new security concerns, which is why your apps do not have connectivity by default. You must add permissions manually in the form of a <uses-permission>; tag in the AndroidManifest.xml.

2.6 Add the Internet permissions

  1. Open the AndroidManifest.xml file.
  2. All permissions of your app need to be put in the AndroidManifest.xml file outside of the <application>; tag. You should be sure to follow the order in which tags are defined in AndroidManifest.xml.
  3. Add the following xml tags outside of the <application> tag:
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    
  4. Build and run your app again. Running a query should now result in a JSON string being printed to the Log.

2.7 Parse the JSON string

Now that you have the correct response to your query, you must parse the results to extract the information you want to display in the UI. Fortunately, Java has existing classes that aids in the parsing and handling of JSON type data. This process, as well as updating the UI, will happen in the onPostExecute() method.

There is chance that the doInBackground() method might not 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 Java JSON methods will fail to parse the data and will throw exceptions. This is why you have to do the parsing in the try block, and the catch block must handle the case where incorrect or incomplete data is returned.

To parse the JSON data and handle possible exceptions, do the following:

  1. In onPostExecute(), add a try/catch block below the call to super.
  2. Use the built-in Java JSON classes (JSONObject and JSONArray) to obtain the JSON array of results items in the try block.
    JSONObject jsonObject = new JSONObject(s);
    JSONArray itemsArray = jsonObject.getJSONArray("items");
    
  3. Iterate through the itemsArray, checking each book for title and author information. If both are not null, exit the loop and update the UI; otherwise continue looking through the list. This way, only entries with both a title and authors will be displayed.

    //Iterate through the results
    for(int i = 0; i<itemsArray.length(); i++){
       JSONObject book = itemsArray.getJSONObject(i); //Get the current item
       String title=null;
       String authors=null;
       JSONObject volumeInfo = book.getJSONObject("volumeInfo");
    
       try {
           title = volumeInfo.getString("title");
           authors = volumeInfo.getString("authors");
       } catch (Exception e){
           e.printStackTrace();
       }
    
       //If both a title and author exist, update the TextViews and return
       if (title != null && authors != null){
           mTitleText.setText(title);
           mAuthorText.setText(authors);
           return;
       }
    }
    
  4. If there are no results which meet the criteria of having both a valid author and a title, and the loop has stopped, set the title TextView to read "No Results Found", and clear the authors TextView.
  5. In the catch block, print the error to the log, set the title TextView to "No Results Found", and clear the authors TextView.

Solution code:

//Method for handling the results on the UI thread
@Override
protected void onPostExecute(String s) {
    super.onPostExecute(s);
    try {
       JSONObject jsonObject = new JSONObject(s);
       JSONArray itemsArray = jsonObject.getJSONArray("items");            
       for(int i = 0; i<itemsArray.length(); i++){
           JSONObject book = itemsArray.getJSONObject(i);
           String title=null;
           String authors=null;
           JSONObject volumeInfo = book.getJSONObject("volumeInfo");


           try {
               title = volumeInfo.getString("title");
               authors = volumeInfo.getString("authors");
           } catch (Exception e){
               e.printStackTrace();
           }


           if (title != null && authors != null){
               mTitleText.setText(title);
               mAuthorText.setText(authors);
               return;
           }
       }


       mTitleText.setText("No Results Found");
       mAuthorText.setText("");


   } catch (Exception e){
       mTitleText.setText("No Results Found");
       mAuthorText.setText("");
       e.printStackTrace();
   }
}

Task 3. Implement UI Best Practices

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

  • When the user clicks Search Books, the keyboard does not disappear, and there is no indication to the user that the query is actually being executed.
  • If there is no network connection, or the search field is empty, the app still tries to query the API and fails without properly updating the UI.
  • If you rotate the screen during a query, the AsyncTask becomes disconnected from the Activity, and it is not able to update the UI with the results.

You will fix these issues in the following section.

3.1 Hide the Keyboard and Update the TextView

The user experience of searching is not intuitive. When the button is pushed, the keyboard remains visible and there is no way to know that the query is in progress. One solution is to programmatically hide the keyboard and update one of the result TextViews to read "Loading…" while the query is being performed. To use this solution, you can:

  1. Add the following code to the searchBooks() method to hide the keyboard when the button is pressed:
    InputMethodManager inputManager = (InputMethodManager)
                         getSystemService(Context.INPUT_METHOD_SERVICE);
    inputManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(),
                         InputMethodManager.HIDE_NOT_ALWAYS);
    
  2. Add a line of code beneath the call to execute the FetchBook task that changes the title TextView to read "Loading…" and clears the author TextView.
  3. Extract your String resources.

3.2 Manage the network state and the empty search field case

Whenever your application uses the network, it needs to handle the possibility that a network connection is unavailable. Before attempting to connect to the network in your AsyncTask or AsyncTaskLoader, your app should check the state of the network connection.

  1. Modify your searchBooks() method to check both the network connection and if there is any text in the search field before executing the FetchBook task.
  2. Update the UI in the case that there is no internet connection or no text in the search field. Display the cause of the error in the TextView.

Solution code:

public void searchBooks(View, view) {

   String queryString = mBookInput.getText().toString();

   InputMethodManager inputManager = (InputMethodManager)
           getSystemService(Context.INPUT_METHOD_SERVICE);
   inputManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(),
           InputMethodManager.HIDE_NOT_ALWAYS);

   ConnectivityManager connMgr = (ConnectivityManager)
           getSystemService(Context.CONNECTIVITY_SERVICE);
   NetworkInfo 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("Please enter a search term");
       } else {
           mAuthorText.setText("");
           mTitleText.setText("Please check your network connection and try again.");
       }
   }
}

Task 4. Migrate to AsyncTaskLoader

When using an AsyncTask, it cannot update the UI if a configuration change occurs while the background task is running. To address this situation, the Android SDK provides a set of classes called loaders designed specifically for loading data into the UI asynchronously. If you use a loader, you don't have to worry about the loader losing the ability to update the UI in the activity that initially created it. The Loader framework does the work for you by reassociating the loader with the appropriate Activity when the device changes its configuration. This means that if you rotate the device while the task is still running, the results will be displayed correctly in the Activity once the data is returned.

In this task you will use a specific loader called an AsyncTaskLoader. An AsyncTaskLoader is an abstract subclass of Loader and uses an AsyncTask to efficiently load data in the background.

Note: When you used an AsyncTask, you implemented the onPostExecute() method in the AsyncTask to display the results on the screen. When you use an AsyncTaskLoader, you define callback methods in the Activity to display the results.

Loaders provide a lot of additional functionality beyond just running tasks and reconnecting to the Activity. For example, you can attach a loader to a data source and have it automatically update the UI elements when the underlying data changes. Loaders can also be programmed to resume loading if interrupted.

So why should you use an AsyncTask if an AsyncTaskLoader is so 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 is not crucial that it updates the UI, an AsyncTask may be sufficient. The Loader framework actually uses an AsyncTask behind the scenes to work its magic.

A good rule of thumb is to use an AsyncTaskLoader instead of an AsyncTask if the user might rotate the screen while the job is running, or when it's critical to update the UI when the job finishes.

In this exercise you will learn how to use a AsyncTaskLoader instead of an AsyncTask to run your Books API query. You will learn more about the uses of other loaders in a later lesson.

Implementing a Loader requires the following components:

Diagram for LoaderManager and the Loader Subclass

  1. The Activity.
  2. The LoaderManager.LoaderCallbacks.
  3. The Loader subclass.
  4. The Loader Implementation.

The LoaderManager automatically moves the loader through its lifecycle depending on the state of the data and the Activity. For example, the LoaderManager calls onStartLoading() when the loader is initialized and destroys the loader when the Activity is destroyed.

The LoaderManager.LoaderCallbacks are a set of methods in the Activity that are called by the LoaderManager when loader is being created, when the data has finished loading, and when the loader is reset. The LoaderCallbacks can take the results of the task and pass them back to the Activity's UI.

The Loader subclass contains the details of loading the data, usually overriding at least onStartLoading(). It can also contain additional features such as observing the data source for changes and caching data locally.

Your Loader subclass implements Loader lifecycle callback methods such as onStartLoading(), onStopLoading() and onReset(). The loader subclass also contains the forceLoad() method which initiates the loading of the data. This method is not called automatically when the loader is started because some setup is usually required before a load is performed. The simplest implementation would call forceLoad() in onStartLoading() which results in a load every time the LoaderManager starts the loader.

4.1 Create an AsyncTaskLoader

  1. Copy the WhoWroteIt project, in order to preserve the results of the previous practical. Rename the copied project WhoWroteItLoader.
  2. Create a new class in your Java directory called BookLoader.
  3. Have your BookLoader class extend AsyncTaskLoader with parameterized type <String>.
  4. Make sure you import the loader from the v4 Support Library.
  5. Implement the required method (loadInBackground()). Notice the similarity between this method and the initial doInBackground() method from AsyncTask.
  6. Create the constructor for your new class. In Android Studio, it's likely the class declaration will still be underlined in red because your constructor does not match the superclass implementation. With your text cursor on the class declaration line, press Alt + Enter (Option + Enter on a Mac) and choose Create constructor matching super. This will create a constructor with the context as a parameter.

Define onStartLoading()

  1. Press Ctrl + O to open the Override methods menu, and select onStartLoading. This method is called by the system when you start your loader.
  2. The loader will not actually start loading the data until you call theforceLoad()method. Inside the onStartLoading()method stub, call forceLoad()to start the loadInBackground()method once the Loader is created.

Define loadInBackground()

  1. Create a member variable mQueryString that will hold the query String, and modify the constructor to take a String as an argument and assign it to the mQueryString variable.
  2. In the loadInBackground()method, call the getBookInfo()method passing in mQueryString, and return the result to download the information from the Books API:
    @Override
    public String loadInBackground() {
       return NetworkUtils.getBookInfo(mQueryString);
    }
    

4.2 Modify MainActivity

You must now implement the Loader Callbacks in your MainActivity to handle the results of the loadInBackground()AsyncTaskLoader method.

  1. Add the LoaderManager.LoaderCallbacks implementation to your Main Activity class declaration, parameterized with the String type:
    public class MainActivity extends AppCompatActivity
                    implements LoaderManager.LoaderCallbacks<String>{
    
  2. Implement all the required methods:onCreateLoader(), onLoadFinished(), onLoaderReset(). Place your text cursor on the class signature line and enter Alt + Enter (Option + Enter on a Mac). Make sure all the methods are selected.

    Note: If the imports for Loader and LoaderManager in MainActivity do not match the import for the AsyncTaskLoader for the BookLoader class, you will have some type errors in the callbacks. Make sure that all imports are from the Android Support Library.

Loaders use the Bundle class to pass information from the calling activity to the LoaderCallbacks. You can add primitive data to a bundle with the appropriate putType()method.

To start a loader, you have two options:

  • initLoader(): This method creates a new loader if one does not exist already, and passes in the arguments Bundle. If a loader exists, the calling Activity is re-associated with it without updating the Bundle.
  • restartLoader(): This method is the same as initLoader() except that if it finds an existing loader, it destroys and recreates it with the new Bundle.

Both of these methods are defined in the LoaderManager, which manages all the Loader instances used in an Activity (or Fragment). Each Activity has exactly one LoaderManager instance that is responsible for the lifecycle of the Loaders that it manages.

Currently, the FetchBook AsyncTask is triggered when the user presses the button. You'll want to start your loader with a new Bundle each time the button is pressed. To do this, you need to edit the onClick method for the button.

  1. In the searchBooks() method, which is the onClick method for the button, replace the call to execute the FetchBook task with a call to restartLoader(), passing in the query string you got from the EditText in the Bundle:

    Bundle queryBundle = new Bundle();
    queryBundle.putString("queryString", queryString);
    getSupportLoaderManager().restartLoader(0, queryBundle,this);
    

    The restartLoader() method takes three arguments:

    • A loader id (useful if you implement more than one loader in your activity).
    • An arguments Bundle (this is where any data needed by the loader goes).
    • The instance of LoaderCallbacks you implemented in your activity. If you want the loader to deliver the results to the MainActivity, specify this as the third argument.
  2. Examine the Override methods in the LoaderCallbacks class. These methods are:

    • onCreateLoader(): Called when you instantiate your Loader.
    • onLoadFinished(): Called when the loader's task finishes. This is where you add the code to update your UI with the results.
    • onLoaderReset(): Cleans up any remaining resources.

You will only be defining the first two methods, since your current data model is a simple string that does not need extra care when the loader is reset.

Implement onCreateLoader()

  1. In onCreateLoader(), return an instance of the BookLoader class, passing in the queryString obtained from the arguments Bundle:
    return new BookLoader(this, args.getString("queryString"));
    

Implement onLoadFinished()

  1. Update onLoadFinished() to process your result, which is the raw JSON String response from the BooksAPI.
    1. Copy the code from onPostExecute() in your FetchBook class to onLoadFinished() in your MainActivity, excluding the call to super.onPostExecute().
    2. Replace the argument to the JSONObject constructor with the passed in data String.
  2. Run your app.

    You should have the same functionality as before, but now in a Loader! One thing still does not work. When the device is rotated, the View data is lost. That is because when the Activity is created (or recreated), the Activity does not know there is a loader running. An initLoader() method is needed in onCreate() of MainActivity to reconnect to the loader.

  3. Add the following code in onCreate() to reconnect to the Loader if it already exists:

    if(getSupportLoaderManager().getLoader(0)!=null){
       getSupportLoaderManager().initLoader(0,null,this);
    }
    
    Note: 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 none to preserve.
  4. Run your app again and rotate the device. The LoaderManager now preserves your data across device configurations!

  5. Remove the FetchBook class as it is no longer used.

Solution code

Android Studio project: WhoWroteItLoader

Coding challenges

Note: All coding challenges are optional and are not prerequisites for later lessons.

Challenge 1: Explore the the specific API you are using in greater detail and find a search parameter that restricts the results to books that are downloadable in the epub format. Add this parameter to your request and view the results.

Challenge 2: The response from the Books API contains as many results as you set with the maxResults parameter, but in this implementation you are only returning the first valid Book result. Modify your app so that the data is displayed in a RecyclerView that has a maxResults amount of entries.

Summary

  • Tasks that connect to the network, or require extra time processing, should not be executed on the UI thread.
    • The Android Runtime usually defaults to StrictMode which will raise an exception if you attempt network connectivity or file access on the UI thread.
  • The Google API Explorer is a tool that helps you explore numerous Google APIs interactively.
    • The Books Search API is a set of RESTful APIs to access Google Books programmatically.
    • An API request to Google Books is in the form of a URL.
    • The response to that API request returns a JSON string.
  • Use getText() to retrieve text from an EditText view. It can be converted into a simple String by using toString().
  • The URI class has a convenient method, Uri.buildUpon() that returns a URI.Builder that can be used to construct a URI string.
  • An AsyncTask is a class that allows you to run tasks in the background, asynchronously, instead of on the UI thread.

    • An AsyncTask can be started via execute().
    • An AsyncTask will not be able to update the UI if the Activity it is controlling terminates (such as in a configuration change on the device).
    • An AsyncTask must be subclassed to be used. The subclass will override at least one method doInBackground(Params) , and most often will override a second one onPostExecute(Result) as well.
  • Whenever an AsyncTask is executed, it goes through the following 4 steps:

    1. onPreExecute(). Invoked on the UI thread before the task is executed. This step is normally used to set up the task.
    2. doInBackground(Params). Invoked on the background thread immediately after onPreExecute() finishes executing. This step is used to perform background computation that can take a long time.
    3. onProgressUpdate(Progress). Invoked on the UI thread after you a call in doInBackground to publishProgress(Progress).
    4. onPostExecute(Result). Invoked on the UI thread after the background computation has finished. The result of the background computation gets passed into this method as a parameter.
  • AsyncTaskLoader is the Loader equivalent of an AsyncTask. It provides a method, loadInBackground(), that runs on a separate thread and whose results are automatically delivered to the UI thread (to the onLoadFinished() LoaderManager callback).
  • You must configure network permissions in the Android manifest file to connect to the Internet:

    <uses-permission android:name="android.permission.INTERNET">
    
  • Use the built in Java JSON classes (JSONObject and JSONArray) to create and parse JSON strings.

  • A Loader allows asynchronous loading of data in an Activity.
    • A Loader can be used to re-establish communication to the UI when an Activity is terminated before the task finishes (such as by device rotation).
    • An AsyncTaskLoader is a Loader that uses an AsyncTask helper class behind the scenes to do work in the background off the main thread.
    • Loaders are managed by a LoaderManager; one or more Loaders can be assigned and managed by a single LoadManager.
    • The LoaderManager allows you to associate a newly created Activity with a Loader using getSupportLoaderManager().initLoader().

The related concept documentation is in Android Developer Fundamentals: Concepts.

Learn more

Android Developer Documentation

Guides

Reference

results matching ""

    No results matching ""