samedi 5 janvier 2013

Populate an Android Spinner with JSON data from a RESTful web API

In the following tutorial I will explain how to populate and Android Spinner (almost equivalent to a java combo box) using JSON data fetched from a REST API. This tutorial DOES NOT explain how to set up and android app or components of an android app; it only explains how to populate an Android spinner. Hoping we are clear with what this does let's begin.

I will use JSON data from the following RESTful API http://www.cmcredit.com/apps/apis/map/

It is a country/region/city database I put together from various sources and you query by sending some NVP parameters. I will also explain how to do this in our android application. Just as a quick example calling the API with the NVP {call: countrylist} will do the get request: http://www.cmcredit.com/apps/apis/map/?call=countrylist and return a country listing. To get regions in a country you could call the api with the NVPs {call: countryregions, country_id: 42}. This API is used for demo purposes ONLY.

We will use data returned from http://www.cmcredit.com/apps/apis/map/?call=countrylist which is of the following format:
{
 "success":true,
 "message":"",
 "data":
  [
   {"country_id":"42","name":"cameroon","iso2":"CM","currency":"XAF"},
   {"country_id":"43","name":"canada","iso2":"CA","currency":"CAD"},
   {"country_id":"44","name":"cape verde","iso2":"CV","currency":"CVE"},
   {"country_id":"45","name":"cayman islands","iso2":"KY","currency":"KYD"},
   {"country_id":"46","name":"central african republic","iso2":"CF","currency":"XAF"},
   {"country_id":"47","name":"chad","iso2":"TD","currency":"XAF"}
  ]
}


Create the various Layouts
1. Activity layout: one TextView and one Spinner (res/layout/activity_spinnerdemo.xml)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical"
    android:paddingTop="30dp" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:paddingLeft="10dp"
        android:text="@string/list_country" >
    </TextView>

    <Spinner
        android:id="@+id/countryField"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:prompt="@string/select_country" >
    </Spinner>

</LinearLayout>


The TextView will simply display the text of the android:text attribute on the screen and the spinner will be populated by our Activity class.

2. The layout of each item in the spinner (res/layout/simple_spinner_item.xml)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal" >

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

    <TextView
        android:id="@+id/item_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#FFFFFF" >
    </TextView>

</LinearLayout>


Define the Activity class
Secondly we define our activity class that will be launched when you start your application (if you set it as the main activity in the AndroidManifest.xml)

SpinnerDemo.java
package com.sewoyebah.examples;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;

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.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.Spinner;
import android.widget.Toast;
import android.annotation.SuppressLint;
import android.app.Activity;

@SuppressLint("DefaultLocale")
public class SpinnerDemo extends Activity {
 // JSON Node names
 private static final String TAG_DATA = "data";
 private static final String TAG_ID_COUNTRY = "country_id";
 private static final String TAG_NAME = "name";
 private static final String TAG_ISO = "iso2";
 private static final String TAG_CURRENCY = "currency";
 private static final String MAP_API_URL = "http://www.cmcredit.com/apps/apis/map";
 private BackGroundTask bgt;

 // Fields
 Spinner countryField;

 ArrayList<Country> countryList = new ArrayList<Country>();

 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_spinnerdemo);
  buildCountryDropDown();
 }

 public void buildCountryDropDown() {
  // Building post parameters, key and value pair
  List<NameValuePair> apiParams = new ArrayList<NameValuePair>(1);
  apiParams.add(new BasicNameValuePair("call", "countrylist"));

  bgt = new BackGroundTask(MAP_API_URL, "GET", apiParams);

  try {
   JSONObject countryJSON = bgt.execute().get();
   // Getting Array of countries
   JSONArray countries = countryJSON.getJSONArray(TAG_DATA);

   // looping through All countries
   for (int i = 0; i < countries.length(); i++) {

    JSONObject c = countries.getJSONObject(i);

    // Storing each json item in variable
	String id = c.getString(TAG_ID_COUNTRY);
	String name = c.getString(TAG_NAME);
	String iso = c.getString(TAG_ISO);
    String currency = c.getString(TAG_CURRENCY);

    // add Country
    countryList.add(new Country(id, name.toUpperCase(), iso, currency));
   }

   // bind adapter to spinner
   countryField = (Spinner) findViewById(R.id.countryField);
   CountryAdapter cAdapter = new CountryAdapter(this, android.R.layout.simple_spinner_item, countryList);
   countryField.setAdapter(cAdapter);
   
   countryField.setOnItemSelectedListener(new OnItemSelectedListener(){

    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
     Country selectedCountry = countryList.get(position);
     showToast(selectedCountry.getName() + " was selected!");
    }

    @Override
    public void onNothingSelected(AdapterView<?> parent) {}
    
   });

  } catch (JSONException e) {
   e.printStackTrace();
  } catch (InterruptedException e) {
   e.printStackTrace();
  } catch (ExecutionException e) {
   e.printStackTrace();
  }
 }

 public void showToast(String msg) {
  Toast.makeText(this, "Toast: " + msg, Toast.LENGTH_LONG).show();
 }
}



Code Explanation

First we define some strings that represent the keys of our JSON data (TAG_DATA to TAG_CURRENCY), then we define our API end point MAP_API_URL.

In Android development ,it is recommended not to make http requests on the same thread that displays the user interface (i.e requests are best made asynchronous and not as a blocking operation). This is why I defined a background class called BackGroundTask.java (Code is at the end of this article), that handles all such request. Android provides a class called AsyncTask which enables asynchronous requests so my BackgroundTask class will just be extending this class and overriding some core functions. You can read more about this from the Android documentation.

Define the Spinner countryField as in the layout and finally we define the ArrayList that will hold the list of countries returned from the the API. Each entry in the ArrayList is of type Country (the definition of Country.java class is also given at the end of this article)

The onCreate is called when your activity is started and in it we set the content view and call our buildCountryDropdown() method.

What happens in the buildCountryDropDown() method?

1. Set the parameters to be sent to the API by defining a NameValuePair list.

List apiParams = new ArrayList(1); 

apiParams.add(new BasicNameValuePair("call", "countrylist"));


2. Start the background task
bgt = new BackGroundTask(MAP_API_URL, "GET", apiParams);

Initiating our background class, we pass as parameters the API end point, the method to use for the request and the NVP parameters. 3. Get response from the background task

JSONObject countryJSON = bgt.execute().get();

The try..catch block that follows is pretty straight forward. From the JSON object shown above, you will notice that the necessary data itself is in a JSON array, which is why to get the data, we use

JSONArray countries = countryJSON.getJSONArray(TAG_DATA);


Each item in the countries JSONArray is a JSONObject so to get the entries of each item in the countries Array, we use

JSONObject c = countries.getJSONObject(i); 


where i is the current position of the iteration. And finally to get the value of each entry you use the getString() method of the JSONObject class. For example to get the id of a country, you use

String id = c.getString(TAG_ID_COUNTRY);


At the end of the for loop after getting the values of the entries in a country, you add the country to the countryList ArrayList defined in the beginning.


countryList.add(new Country(id, name, iso, currency));

(The code defining the country class is given at the end of this topic.)

The last and final thing that needs to be done to complete our demo is to bind the country list to the spinner. This is done using an ArrayAdapter and because we have a custom type Country we define our own custom adapter called CountryAdapter, that extends the ArrayAdapter class. If the list we wanted to bind to the spinner was a list of Strings, we need not define a custom adapter. we counld directly use the ArrayAdapter.

//get country spinner view from the layout
countryField = (Spinner) findViewById(R.id.countryField);
//define adapter to be used when displaying the country list
CountryAdapter cAdapter = new CountryAdapter(this, android.R.layout.simple_spinner_item, countryList);
//bind the adapter to the spinner
countryField.setAdapter(cAdapter);

  
//set a listener for selected items in the spinner  
countryField.setOnItemSelectedListener(new OnItemSelectedListener(){

 @Override
 public void onItemSelected(AdapterView parent, View view, int position, long id) {

  Country selectedCountry = countryList.get(position);
  showToast(selectedCountry.getName() + " was selected!");
 }
    
});


And that will be it. For detailed explanations on Android Classes used in this tutorial, you can visit the Android Developers site.

Other Classes Necessary for this tutorial:

Country.java

package com.sewoyebah.examples;

public class Country {
	private String id;
	private String name;
	private String iso2;
	private String currency;

	public Country(String i, String n, String iso, String curr) {
		id = i;
		name = n;
		iso2 = iso;
		currency = curr;
	}

	public String getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	public String getISO2() {
		return iso2;
	}

	public String getCurency() {
		return currency;
	}

	public String toString() {
		return name;
	}
}



CountryAdapter.java

package com.sewoyebah.examples;

import java.util.ArrayList;

import android.app.Activity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

public class CountryAdapter extends ArrayAdapter<Country> {
	private Activity context;
	ArrayList<Country> data = null;

	public CountryAdapter(Activity context, int resource,
			ArrayList<Country> data) {
		super(context, resource, data);
		this.context = context;
		this.data = data;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) { 
		return super.getView(position, convertView, parent);
	}

	@Override
	public View getDropDownView(int position, View convertView, ViewGroup parent) { 
		View row = convertView;
		if (row == null) {
			LayoutInflater inflater = context.getLayoutInflater();
			row = inflater.inflate(R.layout.simple_spinner_item, parent, false);
		}

		Country item = data.get(position);

		if (item != null) { // Parse the data from each object and set it.
			TextView CountryId = (TextView) row.findViewById(R.id.item_id);
			TextView CountryName = (TextView) row.findViewById(R.id.item_value);
			if (CountryId != null) {
				CountryId.setText(item.getId());
			}
			if (CountryName != null) {
				CountryName.setText(item.getName());
			}

		}

		return row;
	}
}



BackGroundTask.java
package com.sewoyebah.examples;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
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.client.utils.URLEncodedUtils;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONException;
import org.json.JSONObject;

import android.os.AsyncTask;
import android.util.Log;

public class BackGroundTask extends AsyncTask<String, String, JSONObject> {

	List<NameValuePair> postparams = new ArrayList<NameValuePair>();
	String URL = null;
	String method = null;
	static InputStream is = null;
	static JSONObject jObj = null;
	static String json = "";

	public BackGroundTask(String url, String method, List<NameValuePair> params) {
		this.URL = url;
		this.postparams = params;
		this.method = method;
	}

	@Override
	protected JSONObject doInBackground(String... params) {
		// TODO Auto-generated method stub
		// Making HTTP request
		try {
			// Making HTTP request
			// check for request method

			if (method.equals("POST")) {
				// request method is POST
				DefaultHttpClient httpClient = new DefaultHttpClient();
				HttpPost httpPost = new HttpPost(URL);
				httpPost.setEntity(new UrlEncodedFormEntity(postparams));

				HttpResponse httpResponse = httpClient.execute(httpPost);
				HttpEntity httpEntity = httpResponse.getEntity();
				is = httpEntity.getContent();

			} else if (method == "GET") {
				// request method is GET
				DefaultHttpClient httpClient = new DefaultHttpClient();
				String paramString = URLEncodedUtils
						.format(postparams, "utf-8");
				URL += "?" + paramString;
				HttpGet httpGet = new HttpGet(URL);

				HttpResponse httpResponse = httpClient.execute(httpGet);
				HttpEntity httpEntity = httpResponse.getEntity();
				is = httpEntity.getContent();
			}
			
			// read input stream returned by request into a string using StringBuilder
			BufferedReader reader = new BufferedReader(new InputStreamReader(is, "utf-8"), 8);
			StringBuilder sb = new StringBuilder();
			String line = null;
			while ((line = reader.readLine()) != null) {
				sb.append(line + "\n");
			}
			is.close();
			json = sb.toString();
			
			// create a JSONObject from the json string
			jObj = new JSONObject(json);

		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		} catch (ClientProtocolException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (JSONException e) {
			Log.e("JSON Parser", "Error parsing data " + e.toString());
		} catch (Exception e) {
			Log.e("Buffer Error", "Error converting result " + e.toString());
		}

		// return JSONObject (this is a class variable and null is returned if something went bad)
		return jObj;

	}
}



Ignore the closing tags at the end of the code snippets, the script formatting the code snippets just messes up.

3 commentaires:

  1. In the County Adapter class what is the variable R.layout.dropdown_value_id a reference to? A spinner id?

    Edit never mind i see its a reference to a layout.... however i dont see that layout in this tutorial?

    nice work by the way

    RépondreSupprimer
  2. Ce commentaire a été supprimé par l'auteur.

    RépondreSupprimer
  3. Este ce que on peut faire le download du code?
    C'est plus facile, pou pouvoi voir le code et le tester avec un format de Json que je suis en train d'utilizer.

    Je vous remercie.

    RépondreSupprimer