Android fundamentals 07.2:AsyncTask and AsyncTaskLoader
Tutorial source : Google CodeLab
Date : 2021/04/06
Complete course : 教程目录 (java).
Note : The link in this article requires Google access
1、Welcome
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:
- For the complete list of codelabs in the course, see Codelabs for Android Developer Fundamentals (V2).
- For details about the course, including links to all the concept chapters, apps, and slides, see Android Developer Fundamentals (Version 2).
Note: This course uses the terms “codelab” and “practical” interchangeably.
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:
- Create an activity.
- Add a
TextView
to the layout for the activity. - Implement
onClick
functionality for a button in your layout. - Implement an
AsyncTask
and displaying the result in your UI. - Pass information between activities as extras.
What you’ll learn
- How to use the Google APIs Explorer to investigate Google APIs and to view JSON responses to HTTP requests.
- How to use the Google Books API to retrieve data over the internet and keep the UI fast and responsive. You won’t learn the Books API in detail—your app will only use the simple book-search function.
- How to parse the JSON results from your API query.
- How to implement an
AsyncTaskLoader
that preserves data on configuration changes. - How to update your UI using the loader callbacks.
What you’ll do
- Use the Google APIs Explorer to learn about the Books API.
- Create the “Who Wrote It?” app, which 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 anAsyncTask
.
2、App overview
You will build an app that contains an EditText
and a Button
.
- The user enters the name of the book in the
EditText
and taps the button. - The button executes an
AsyncTask
that queries the Google Books API to find the author and title of the book the user is looking for. - The results are retrieved and displayed in a
TextView
below the button.
Once the app is working, you modify the app to use AsyncTaskLoader
instead of the AsyncTask
class.
3、Task 1. Explore the Google Books API
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
- Go to the Google APIs Explorer at https://developers.google.com/apis-explorer/.
- Click Services in the left nav, and then Books API.
- 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.
- In the
q
field, enter a book name or a partial book name, for example “Romeo”. Theq
parameter is the only required field. - In the
maxResults
field, enter10
to limit the results to the top 10 matching books. - In the
printType
field, enterbooks
to limit the results to books that are in print. - Make sure that the Authorize requests using OAuth 2.0 switch at the top of the form is off.
- Click Execute without OAuth link at the bottom of the form.
- 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.
- Find the value for the
"title"
name for one book. Notice that this result contains a single value. - 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.
4、Task 2. Create the “Who Wrote It?” app
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)
- Create a new project called “WhoWroteIt”, using the Empty Activity template. Accept the defaults for all the other options.
- Open the
activity_main.xml
layout file. Click the Text tab. - Add the
layout_margin
attribute to the top-levelConstraintLayout
:
android:layout_margin="16dp"
- Delete the existing
TextView
. - 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_widthandroid:layout_heightandroid:idandroid:textandroid:textAppearanceapp:layout_constraintStart_toStartOfapp:layout_constraintTop_toTopOf |
"match_parent""wrap_content""@+id/instructions""@string/instructions""@style/TextAppearance. AppCompat.Title""parent""parent"
|
EditText |
android:layout_widthandroid:layout_heightandroid:idandroid:layout_marginTopandroid:inputTypeandroid:hintapp:layout_constraintEnd_toEndOfapp:layout_constraintStart_toStartOfapp:layout_constraintTop_toBottomOf |
"match_parent""wrap_content""@+id/bookInput""8dp""text""@string/input_hint""parent""parent""@+id/instructions" |
Button |
android:layout_widthandroid:layout_heightandroid:idandroid:layout_marginTopandroid:textandroid:onClickapp:layout_constraintStart_toStartOfapp:layout_constraintTop_toBottomOf |
"wrap_content""wrap_content""@+id/searchButton""8dp""@string/button_text""searchBooks""parent""@+id/bookInput" |
TextView |
android:layout_widthandroid:layout_heightandroid:idandroid:layout_marginTopandroid:textAppearanceapp:layout_constraintStart_toStartOfapp:layout_constraintTop_toBottomOf |
"wrap_content""wrap_content""@+id/titleText""16dp""@style/TextAppearance.AppCompat.Headline""parent""@+id/searchButton" |
TextView |
android:layout_widthandroid:layout_heightandroid:idandroid:layout_marginTopandroid:textAppearanceapp:layout_constraintStart_toStartOfapp:layout_constraintTop_toBottomOf |
"wrap_content""wrap_content""@+id/authorText""8dp""@style/TextAppearance. AppCompat.Headline""parent""@+id/titleText" |
- 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>
- The
onClick
attribute for the button will be highlighted in yellow, because thesearchBooks()
method is not yet implemented inMainActivity
. To create the method stub inMainActivity
, place your cursor in the highlighted text, pressAlt+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
.
- In
MainActivity.java
, create member variables for theEditText
, the authorTextView
, and the titleTextView
.
private EditText mBookInput;
private TextView mTitleText;
private TextView mAuthorText;
- 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);
}
- In the
searchBooks()
method, get the text from theEditText
view. Convert the text to aString
, 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.
- Create a Java class in your app called
FetchBook
, that extendsAsyncTask
. 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, andString
because the JSON response is a string.)
public class FetchBook extends AsyncTask<String, Void, String> {
}
- Implement the required method,
doInBackground``()
. To do this, place your cursor on the red underlined text, pressAlt+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;
}
- Select Code > Override methods, or press
Ctrl+O
. Select the onPostExecute() method to insert the method definition into the class. TheonPostExecute()
method takes aString
as a parameter and returnsvoid
.
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
}
- To display the results in the
TextView
objects inMainActivity
, you must have access to those text views inside theAsyncTask
. CreateWeakReference
member variables for references to the two text views that show the results.
private WeakReference<TextView> mTitleText;
private WeakReference<TextView> mAuthorText;
Note: As you learned in the previous practical, you use WeakReference
objects for these text views (rather than actual TextView
objects) to avoid leaking context from the Activity
. The weak references prevent memory leaks by allowing the object held by that reference to be garbage-collected if necessary.
- Create a constructor for the
FetchBook
class that includes theTextView
views fromMainActivity
, 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
.
- Create a new Java class in your app called
NetworkUtils
. TheNetworkUtils
class does not extend from any other class. - For logging, create a
LOG_TAG
variable with the name of the class:
private static final String LOG_TAG =
NetworkUtils.class.getSimpleName();
- Create a static method named
getBookInfo()
. ThegetBookInfo()
method takes the search term as aString
parameter and returns the JSONString
response from the API you examined earlier.
static String getBookInfo(String queryString){
}
- 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;
- At the end of the
getBookInfo()
method, return the value ofbookJSONString
.
return bookJSONString;
- Add a skeleton
try
/catch
/finally
block ingetBookInfo()
, after the local variables and before thereturn
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 {
//...
}
- Create the following member constants at the top of the the
NetworkUtils
class, below theLOG_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.
- In the
getBookInfo()
method, build your request URI in thetry
block:
Uri builtURI = Uri.parse(BOOK_BASE_URL).buildUpon()
.appendQueryParameter(QUERY_PARAM, queryString)
.appendQueryParameter(MAX_RESULTS, "10")
.appendQueryParameter(PRINT_TYPE, "books")
.build();
- 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.
- In the
try
block of thegetBookInfo()
method, open the URL connection and make the request:
urlConnection = (HttpURLConnection) requestURL.openConnection();
urlConnection.setRequestMethod("GET");
urlConnection.connect();
- Also inside the
try
block, set up the response from the connection using anInputStream
, aBufferedReader
and aStringBuilder
.
// 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();
- 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");
}
Note: The while
loop adds the incoming line to the builder string in two steps: one step for the line of response data, and one step to add the new line character ("\n"
).
The new line does not affect JSON parsing of the response, but it makes it a lot easier to debug the response when you view it in the log.
- 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;
}
- Convert the
StringBuilder
object to aString
and store it in thebookJSONString
variable.
bookJSONString = builder.toString();
- In the
finally
block, close both the connection and theBufferedReader
:
finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Note: Each time the connection fails for any reason, this code returns null
. This means that the onPostExecute()
in the FetchBook
class has to check its input parameter for a null
string and let the user know about the failure.
This error handling strategy is simplistic, because the user has no idea why the connection failed. A better solution for a production app is to handle each point of failure differently so that the user gets helpful feedback.
- Just before the final return, print the value of the
bookJSONString
variable to the log.
Log.d(LOG_TAG, bookJSONString);
- In
FetchBook
, modify thedoInBackground()
method to call theNetworkUtils.getBookInfo()
method, passing in the search term that you obtained from theparams
argument passed in by the system. (The search term is the first value in thestrings
array.) Return the result of this method. (This line replaces thenull
return.)
return NetworkUtils.getBookInfo(strings[0]);
- In
MainActivity
, add this line to the end of thesearchBooks()
method to launch the background task with theexecute()
method and the query string.
new FetchBook(mTitleText, mAuthorText).execute(queryString);
- 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
- Open the
AndroidManifest.xml
file. - 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" />
- 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.
- In the
FetchBook
class, in theonPostExecute()
method, add atry
/catch
block below the call tosuper
.
try {
//...
} catch (JSONException e) {
e.printStackTrace();
}
- Inside the
try
block, use the classesJSONObject
andJSONArray
to obtain the JSON array of items from the result string.
JSONObject jsonObject = new JSONObject(s);
JSONArray itemsArray = jsonObject.getJSONArray("items");
- Initialize the variables used for the parsing loop.
int i = 0;
String title = null;
String authors = null;
- 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++;
}
Note: The loop ends at the first match in the response. More responses might be available, but this app only displays the first one.
- If a matching response is found, update the UI with that response. Because the references to the
TextView
objects areWeakReference
objects, you have to dereference them using theget()
method.
if (title != null && authors != null) {
mTitleText.get().setText(title);
mAuthorText.get().setText(authors);
}
- 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 authorTextView
.
} else {
mTitleText.get().setText(R.string.no_results);
mAuthorText.get().setText("");
}
- In the
catch
block, print the error to the log. Set the titleTextView
to the “no results” string resource, and clear the authorTextView
.
} 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();
}
- Add the
no_results
resource tostrings.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("");
}
}
5、Task 3. Implement UI best practices
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:
- When the user clicks Search Books, the keyboard does not disappear. The user has no indication that the query is being executed.
- If there is no network connection, or if 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 theActivity
, and it is not able to update the UI with results.
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.
- In
MainActivity
, add the following code to thesearchBooks()
method, after thequeryString
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);
}
- Just beneath the call to execute the
FetchBook
task, add code to change the titleTextView
to a loading message and clear the authorTextView
.
new FetchBook(mTitleText, mAuthorText).execute(queryString);
mAuthorText.setText("");
mTitleText.setText(R.string.loading);
- Add the
loading
resource tostrings.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.
- In the
searchBooks()
method, use theConnectivityManager
andNetworkInfo
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();
}
- Add a test around the call to the
FetchBook
task andTextView
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);
}
- Add an
else
block to that test. In theelse
block, update the UI with ano_search_term
error message if there is no term to search for, and ano_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);
}
}
- Add the
no_search_term
andno_network
resources tostrings.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.
6、Task 4. Migrate to AsyncTaskLoader
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.
Note: The AsyncTaskLoader
class is part of the Android platform’s Loader
API, which is a framework to manage loading data into your app in the background. Loaders were deprecated in Android P (API 28) in favor of ViewModels
and LiveData
.
The AsyncTaskLoader
class is still available, but for full backward-compatibility, make sure to use the AsyncTaskLoader
and other related classes from the Android Support Library.
In this exercise you learn how to use AsyncTaskLoader
instead of AsyncTask
to run your Books API query.
4.1 Create an AsyncTaskLoader class
- To preserve the results of the previous practical, copy the WhoWroteIt project. Rename the copied project “WhoWroteItLoader”.
- Create a class called
BookLoader
that extendsAsyncTaskLoader
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.
- Implement the required
loadInBackground()
method. Notice the similarity between this method and the initialdoInBackground()
method fromAsyncTask
.
@Nullable
@Override
public String loadInBackground() {
return null;
}
- Create the constructor for the
BookLoader
class. With your text cursor on the class declaration line, pressAlt+Enter
(Option+Enter
on a Mac) and select Create constructor matching super. This creates a constructor with theContext
as a parameter.
public BookLoader(@NonNull Context context) {
super(context);
}
4.2 Implement required methods
- 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();
}
- Inside the
onStartLoading()
method stub, callforceLoad()
to start theloadInBackground()
method. The loader will not start loading data until you call theforceLoad()
method.
@Override
protected void onStartLoading() {
super.onStartLoading();
}
- Create a member variable called
mQueryString
to hold the string for the Books API query. Modify the constructor to take aString
as an argument and assign it to themQueryString
variable.
private String mQueryString;
BookLoader(Context context, String queryString) {
super(context);
mQueryString = queryString;
}
- In the
loadInBackground()
method, replace the return statement with the following code, which calls theNetworkUtils
.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.
- In
MainActivity
, add theLoaderManager.LoaderCallbacks
implementation to the class declaration, parameterized with theString
type:
public class MainActivity extends AppCompatActivity
implements LoaderManager.LoaderCallbacks<String> {
Make sure to import the LoaderManager.LoaderCallbacks
class from the v4 Support Library.
- Implement all the required callback methods from the interface. Thi includes
onCreateLoader()
,onLoadFinished()
, andonLoaderReset()
. Place your cursor on the class signature line and pressAlt+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 onl oadFinished(@NonNull Loader<String> loader, String data) {
}
@Override
public void onl oaderReset(@NonNull Loader<String> loader) {
}
About the required methods:
-
onCreateLoader()
is called when you instantiate your loader. -
onLoadFinished()
is 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.
For this app, you only implement the first two methods. Leave onLoaderReset()
empty.
- The
searchBooks()
method is the onClick method for the button. InsearchBooks()
, replace the call to execute theFetchBook
task with a call torestartLoader()
. Pass in the query string that you got from theEditText
in the loader’sBundle
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:
- A loader
id
, which is useful if you implement more than one loader in your activity. - An arguments
Bundle
for any data that the loader needs. - The instance of
LoaderCallbacks
that you implemented in your activity. If you want the loader to deliver the results to theMainActivity
, specifythis
as the third argument.
4.4 Implement loader callbacks
In this task you implement the onCreateLoader()
and onLoadFinished()
callback methods to handle the background task.
- In
onCreateLoader()
, replace thereturn
statement with a statement that returns an instance of theBookLoader
class. Pass in the context (this
) and thequeryString
obtained from the passed-inBundle
:
@NonNull
@Override
public Loader onCreateLoader(int id, @Nullable Bundle args) {
String queryString = "";
if (args != null) {
queryString = args.getString("queryString");
}
return new BookLoader(this, queryString);
}
- Copy the code from
onPostExecute()
in your FetchBook class toonLoadFinished()
in yourMainActivity
. Remove the call tosuper.onPostExecute()
. This is the code that parses the JSON result for a match with the query string. - Remove all the calls to
get()
for each of theTextView
objects. Because updating the UI happens in theActivity
itself, you no longer need weak references to the original views. - Replace the argument to the
JSONObject
constructor (the variables
) with the parameterdata
.
JSONObject jsonObject = new JSONObject(data);
- 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 theonCreate()
ofMainActivity
. - 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.
- Run your app again and rotate the device. The loader manager now holds onto your data across device-configuration changes!
- 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.
7、Coding challenge
Note: All coding challenges are optional and are not a prerequisite for later lessons.
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.
8、Summary
- Tasks that connect to the network should not be executed on the UI thread. The Android runtime usually raises an exception if you attempt network connectivity or file access on the UI thread.
- Use the Books Search API to access Google Books programmatically. An API request to Google Books is in the form of a URL, and the response is a JSON string.
- Use the Google APIs Explorer to explore Google APIs interactively.
- Use
getText()
to retrieve text from anEditText
view. To convert the text into a simple string, usetoString()
. - The
Uri.buildUpon()
method returns aURI.Builder
that you can use to construct URI strings. - To connect to the internet, you must configure network permission in the Android manifest file:
<uses-permission android:name="android.permission.INTERNET" />
The AsyncTask
class lets you run tasks in the background instead of on the UI thread:
- To use an
AsyncTask
, you have to subclass it. The subclass overrides thedoInBackground(Params...)
method. Usually the subclass also overrides theonPostExecute(Result)
method. - To start an
AsyncTask
, useexecute()
. - An
AsyncTask
can’t update the UI if the activity that theAsyncTask
is controlling stops, for example because of a device-configuration change.
When an AsyncTask
executes, it goes through four steps:
-
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. -
doInBackground(Params...)
runs on the background thread immediately afteronPreExecute()
finishes. This step performs background computations that can take a long time. -
onProgressUpdate(Progress...)
runs on the UI thread after you a callpublishProgress(Progress...)
. -
onPostExecute(Result)
runs on the UI thread after the background computation is finished. The result of the computation is passed toonPostExecute()
.
AsyncTaskLoader
is the loader equivalent of an AsyncTask
.
-
AsyncTaskLoader
provides theloadInBackground()
method, which runs on a separate thread. - The results of
loadInBackground()
are delivered to the UI thread by way of theonLoadFinished()
LoaderManager
callback. - To create and parse JSON strings, use the built-in Java JSON classes
JSONObject
andJSONArray
. - An
AsyncTaskLoader
uses anAsyncTask
helper class to do work in the background, off the main thread. -
AsyncTaskLoader
instances are managed by aLoaderManager
. - The
LoaderManager
lets you associate a newly createdActivity
with a loader usinggetSupportLoaderManager().initLoader()
.
9、Related concepts
The related concept documentation is in 7.2: Internet connection.
10、Learn more
Android developer documentation:
11、Homework
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:
- Assign homework if required.
- Communicate to students how to submit homework assignments.
- Grade the homework assignments.
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:
- A field in which the user enters a URL
- A field such as a menu or spinner that allows the user to choose the protocol (HTTP or HTTPS)
- A button that executes the task when the user taps it
- A scrolling display of the source code of the web page at the URL
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?
android.permission.CONNECTIVITY
android.permission.INTERNET
- It doesn’t need any special permissions, because all Android apps are allowed to connect to the internet.
Question 2
How does your app check that internet connectivity is available?
In the manifest:
- request
ACCESS_NETWORK_STATE
permission - request
ALL_NETWORK_STATE
permission - request
NETWORK_CONNECT
permission
In the code:
- Wrap the code to connect to the internet in a
try
/catch
block, and catchNO_NETWORK
errors. - Use
ConnectivityManager
to check for an active network before connecting to the network. - Present a dialog to the user reminding them to make sure that internet connectivity is available before they attempt to connect to the internet.
Question 3
Where do you implement the loader callback method that’s triggered when the loader finishes executing its task?
- In the
AsyncTaskLoader
subclass. TheAsyncTaskLoader
must implementLoaderManager.LoaderCallbacks
. - In the
Activity
that displays the results of the task. TheActivity
must implementLoaderManager.LoaderCallbacks
. - In a
Utility
class that extendsObject
and implementsLoaderManager.LoaderCallbacks
.
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?
- A running
AsyncTask
becomes disconnected from the activity, but keeps running. A runningAsyncTaskLoader
becomes disconnected from the activity and stops running, preserving system resources. - A running
AsyncTask
becomes disconnected from the activity and stops running, preserving system resources. A runningAsyncTaskLoader
automatically restarts execution of its task from the beginning. The activity displays the results. - A running
AsyncTask
becomes disconnected from the activity, but keeps running. A runningAsyncTaskLoader
automatically reconnects to the activity after the device rotation. The activity displays the results.
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?
- In
onCreateLoader()
in the activity, create an instance of theAsyncTaskLoader
subclass. In the loader’s constructor, perform initialization tasks. - In
onCreateLoader()
in the activity, create an instance of theAsyncTaskLoader
subclass. In the loader’sinit()
method, perform initialization tasks. - In the
Activity
, implementinitLoader()
to initialize the loader. - Perform initialization tasks for the loader at the start of
loadInBackgroud()
in theLoader
.
Question 6
What methods must an AsyncTaskLoader
implement?
Submit your app for grading
Guidance for graders
Check that the app has the following features:
- The manifest includes requests for the appropriate permissions.
- Uses a subclass of
AsyncTaskLoader
. - Responds appropriately if the device can’t connect to the internet.
- Combines the protocol and the web page to create a valid URL that the app uses to connect to the internet.
- Implements the required
Loader
callback methods. - Displays the results of retrieving the source of the web page in a
TextView
in a ScrollView. (It’s OK to do it in the same activity, or to start a new activity.)
12、Next codelab
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).