lundi 21 octobre 2013

Android: Re-using AsyncTask class across multiple Activities.

First I will like to start by apologizing for not putting this up a long time ago after several email request. I have very little time to put these Tutorials together for public consumption and I prefer responding with short snippets via email. Anyway.. that aside.

This post assumes you have a basic understanding of Android Development precisely about Activities and AsyncTasks. You can visit respective links to learn more on the official android documentation.

I will try to explain how to use a single Async class across Activities that make asynchronous request to a web service.

Suppose you have a REST API that always responds with some JSON output (which we will get as a string) and you need to call this API in multiple (greater than two) Activities. It will be good to have a common AsyncTask class, where you only pass an entry-point URL and a set of parameters and it returns the response. If you however have only a single activity that does some asynchronous task, it will be good design to implement it's AsyncTask class as a private sub class. So, how can we go about implementing a single AsyncTask class to be used in multiple Activities?


  1. We will implement a sub class of the AsyncTask class which will define an interface that MUST be implemented in the calling Activity. This subclass will take as parameters the calling activity (type: Activity), method (POST or GET type: String) and a set of parameters (type:  List<namevaluepair>). Three constructors will be implemented to make the last two parameters optional.
  2. We will show a ProgressDialog while the request is being executed in the background.
  3. We demonstrate the use of this class by defining and example Activity.

A. The subclass: AsyncRequest.java

package com.sewoyebah.examples;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URLEncoder;
import java.util.List;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.AsyncTask;

public class AsyncRequest extends AsyncTask<String, Integer, String> {

 OnAsyncRequestComplete caller;
 Context context;
 String method = "GET";
 List<NameValuePair> parameters = null;
 ProgressDialog pDialog = null;

 // Three Constructors
 public AsyncRequest(Activity a, String m, List<NameValuePair> p) {
  caller = (OnAsyncRequestComplete) a;
  context = a;
  method = m;
  parameters = p;
 }

 public AsyncRequest(Activity a, String m) {
  caller = (OnAsyncRequestComplete) a;
  context = a;
  method = m;
 }

 public AsyncRequest(Activity a) {
  caller = (OnAsyncRequestComplete) a;
  context = a;
 }

 // Interface to be implemented by calling activity
 public interface OnAsyncRequestComplete {
  public void asyncResponse(String response);
 }

 public String doInBackground(String... urls) {
  // get url pointing to entry point of API
  String address = urls[0].toString();
  if (method == "POST") {
   return post(address);
  }

  if (method == "GET") {
   return get(address);
  }

  return null;
 }

 public void onPreExecute() {
  pDialog = new ProgressDialog(context);
  pDialog.setMessage("Loading data.."); // typically you will define such
            // strings in a remote file.
  pDialog.show();
 }

 public void onProgressUpdate(Integer... progress) {
  // you can implement some progressBar and update it in this record
  // setProgressPercent(progress[0]);
 }

 public void onPostExecute(String response) {
  if (pDialog != null && pDialog.isShowing()) {
   pDialog.dismiss();
  }
  caller.asyncResponse(response);
 }

 protected void onCancelled(String response) {
  if (pDialog != null && pDialog.isShowing()) {
   pDialog.dismiss();
  }
  caller.asyncResponse(response);
 }

 @SuppressWarnings("deprecation")
 private String get(String address) {
  try {

   if (parameters != null) {

    String query = "";
    String EQ = "="; String AMP = "&";
    for (NameValuePair param : parameters) {
     query += param.getName() + EQ + URLEncoder.encode(param.getValue()) + AMP;
    }

    if (query != "") {
     address += "?" + query;
    }
   }

   HttpClient client = new DefaultHttpClient();
   HttpGet get= new HttpGet(address);

   HttpResponse response = client.execute(get);
   return stringifyResponse(response);

  } catch (ClientProtocolException e) {
   // TODO Auto-generated catch block
  } catch (IOException e) {
   // TODO Auto-generated catch block
  }

  return null;
 }

 private String post(String address) {
  try {

   HttpClient client = new DefaultHttpClient();
   HttpPost post = new HttpPost(address);

   if (parameters != null) {
    post.setEntity(new UrlEncodedFormEntity(parameters));
   }

   HttpResponse response = client.execute(post);
   return stringifyResponse(response);

  } catch (ClientProtocolException e) {
   // TODO Auto-generated catch block
  } catch (IOException e) {
   // TODO Auto-generated catch block
  }

  return null;
 }

 private String stringifyResponse(HttpResponse response) {
  BufferedReader in;
  try {
   in = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));

   StringBuffer sb = new StringBuffer("");
   String line = "";
   while ((line = in.readLine()) != null) {
    sb.append(line);
   }
   in.close();

   return sb.toString();
  } catch (IllegalStateException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (IOException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }

  return null;
 }
}

First we define the three constructors that our class should support, the first supports three parameters, the second two and the third one. Next we define the interface which the calling
Activity must implement and hence override the asyncResponse() method. Next we implement the required inherited methods. Depending on the method specified, we call the get() or post() private functions
which will return the server response that will be passed to the onPostExecute() method and back to our Activity via the implemented interface.

Please read the official Documentation on AsyncTask to have an understanding of the inherited methods. In summary onPreExecute() is called just before the request is sent, doInBackgroud() is what actually does the job and returns a value which a passed to onPostExecute() called after request is completed. If request was cancelled, onCancelled() is called instead of onPostExecute(). A typical example of a canceled request is when user rotates a device when request is not completed. If you didn't handle the activity life cycle properly then Activity will be redrawn and another request will be made (cancelling the previous).

We also use here a progressDialog to indicate to the user something  is being executed. It is initialized in the onPreExecute() method because we don't want to do this in each constructor of the class and besides we still have hold of the UI thread in the onPreExecute() method.

B. The Example Activity: PostsActivity.java

package com.sewoyebah.examples;

import java.util.ArrayList;

import org.apache.http.NameValuePair;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.os.Bundle;
import android.app.Activity;
import android.widget.TextView;

public class PostsActivity extends Activity implements
  AsyncRequest.OnAsyncRequestComplete {

 TextView titlesView;
 String apiURL = "http://www.example.com/apis/posts";
 ArrayList<NameValuePair> params;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_posts);

  // TODO Auto-generated method stub
  titlesView = (TextView) findViewById(R.id.post_titles);
  params = getParams();
  AsyncRequest getPosts = new AsyncRequest(this, "GET", params);
  getPosts.execute(apiURL);
 }

 // override method from AsyncRequest.OnAsyncRequestComplete interface
 // the response can contain other parameters specifying if the request was
 // successful or not and other things
 // in a real world application various checks will be done on the response
 @Override
 public void asyncResponse(String response) {

  try {
   // create a JSON array from the response string
   JSONArray objects = new JSONArray(response);
   // define a string to hold out titles (in a real word application you will be using a ListView and an Adapter to do such listing)
   String titles = "";
   String NL = "\n";
   String DOT = ". ";
   for (int i = 0; i < objects.length(); i++) {
    JSONObject object = (JSONObject) objects.getJSONObject(i);
    titles += object.getString("id") + DOT + object.getString("title") + NL;
   }
   titlesView.setText(titles);
  } catch (JSONException e) {
   e.printStackTrace();
  }
 }

 // here you specify and return a list of parameter/value pairs supported by
 // the API
 private ArrayList<NameValuePair> getParams() {
  // define and ArrayList whose elements are of type NameValuePair
  ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
  params.add(new BasicNameValuePair("start", "0"));
  params.add(new BasicNameValuePair("limit", "10"));
  params.add(new BasicNameValuePair("fields", "id,title"));
  return params;
 }

}

This is a pretty straight forward Android Activity. In this example we will assume our response is a JSON array. That is, an array of JSON Objects. For example:

 [{"id": 1, "title": "Title of post 1"}, {"id": 2, "title": "Title of post 2"}, {"id": 3, "title": "Title of post 3"}];

Observe that the class implements the AsyncRequest.OnAsyncRequestComplete interface of the AsyncRequest and hence must override the asyncResponse() method of this interface.

To execute the request in the onCreate() method, we define the set of parameters using the getParams() method, create an instance of the AsyncRequest class and call it's execute() method passing to it the entry point (URL) of your API. It is common to use and ArrayList of NameValuePairs as a paramter set.

The response will be received and parsed as a String by our AsyncRequest class and passed to the asyncResponse() interface method as a string.

In the asyncResponse(), we create a JSONArray from this string and read each object and display a list of titles. In a typical real world application, you will implement this using a ListView and an Adapter but this is out of the scope of this tutorial. Here we just concatenate the titles as a String and display in a TextView.


C. Another Example Activity: PostActivity.java

Another example of an activity could be one that gets the details of a post using the post ID. This will typically be launched as an Intent from the Listing Activity. In my next post I will try to explain the same concept using a ListView and an ArrayAdapter to display a list of posts and onClick opens the details of a post in another activity

package com.sewoyebah.examples;

import java.util.ArrayList;

import org.apache.http.NameValuePair;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONException;
import org.json.JSONObject;

import android.os.Bundle;
import android.app.Activity;
import android.widget.TextView;

public class PostActivity extends Activity implements
  AsyncRequest.OnAsyncRequestComplete {

 TextView postView;
 String apiURL = "http://www.example.com/apis/posts";
 ArrayList<NameValuePair> params;
 String postID = "75";

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_post);

  // TODO Auto-generated method stub
  postView = (TextView) findViewById(R.id.post_view);
  params = getParams();
  AsyncRequest getPosts = new AsyncRequest(this, "GET", params);
  getPosts.execute(apiURL);
 }

 // override method from AsyncRequest.OnAsyncRequestComplete interface
 @Override
 public void asyncResponse(String response) {

  try {
   // create a JSON array from the response string
   JSONObject postObject = new JSONObject(response);
   // define a string to hold out titles
   // (in a real word application you will be using a ListView and an
   // Adapter to do such listing)
   String post = "";
   String NL = "\n";
   post += postObject.getString("title") + NL + NL;
   post += postObject.getString("description");
   postView.setText(post);
  } catch (JSONException e) {
   e.printStackTrace();
  }
 }

 // here you specify and return a list of parameter/value pairs supported by
 // the API
 private ArrayList<NameValuePair> getParams() {
  // define and ArrayList whose elements are of type NameValuePair
  ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
  params.add(new BasicNameValuePair("id", postID));
  return params;
 }

}
In case you want to fully test code, bellow are the Layout xml files:

/res/layout/activity_posts.xml
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".PostsActivity" >

    <TextView
        android:id="@+id/post_titles"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>

/res/layout/activity_post.xml
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".PostActivity" >

    <TextView
        android:id="@+id/post_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>
So in summary after defining the AsyncRequest class this is what you do for each Activity that is to use it:

Class definition
public class MyActivity extends Activity implements AsyncRequest.OnAsyncRequestComplete

Define set of paramters:

ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("id", "75"));
params.add(new BasicNameValuePair("attributes", "title,description"));

Start our async task

AsyncRequest request = new AsyncRequest(this, 'GET', params);
request.execute(apiURL);

And implement our asyncResponse() method which will be called after the async request is completed

public void asyncResponse(String response) { ... }

Sending multiple asynchronous requests in same activity

One will now ask: Is it possible to send multiple asynchronous requests from same activity using the AsyncRequest class? The answer is yes but you need to add a bit more to the above solution and keep in mind there is a limit to the number of threads you can run simultaneously based on the API version your app supports. See Android documentation for details.

First you assign a label to each request and pass that label to the AsyncRequest Class and back to the asyncResponse()  method. Your constructor could be like

public AsyncRequest(Activity a, String m, List<NameValuePair> p, String l) {
  caller = (OnAsyncRequestComplete) a;
  context = a;
  method = m;
  parameters = p;
  label = l;
 }

And your interface defined as

public interface OnAsyncRequestComplete {
 public void asyncResponse(String response, String label);
}

and when calling the interface after the request is complete in the postExecute() or isCancelled()  methods you do

caller.asyncResponse(response, label);

and finally back in the Activity you implement your asyncResponse() method as follows

public void asyncResponse(String response, String label) {

 swtich(label) {
  case "get_posts_request"
  // call some method to complete the request
  // you initialized your class with new AsyncRequest(this, "GET", params, "get_posts_request")
  completeGetPostRequest(response);
  break;
  
  case "some_other_label"
  // call some method to complete the request
  // you initialized your class with new AsyncRequest(this, "POST", params, "some_other_label")
  completeSomeOtherRequest(response);
  break;
 }
   
}


Hope this gives some insights..

9 commentaires:

  1. Hi this code was working in fine in activity but not working in Fragment

    RépondreSupprimer
  2. maybe this is too late..
    But.. to make this work in Fragments you can add another custom Constructor:

    public AsyncRequest(OnAsyncRequestComplete c, Activity a, String m, List p, String l) {
    caller = c;
    context = a;
    method = m;
    parameters = p;
    label = l;
    }

    and call it like this:
    AsyncRequest getPosts = new AsyncRequest(TheFragment.this,getActivity(), "GET", params);

    RépondreSupprimer

  3. It's interesting that many of the bloggers your tips helped to clarify a few things for me as well as giving... very specific nice content.Android Training institute in chennai with placement | Android Training in chennai

    RépondreSupprimer
  4. This information is impressive; I am inspired with your post writing style & how continuously you describe this topic. After reading your post, thanks for taking the time to discuss this, I feel happy about it and I love learning more about this topic.Android Training|Android Training in chennai with placement | Android Training in velachery

    RépondreSupprimer