Thursday 27 February 2014

THE BEST way to access data from web in Android


We need to access data from web in more than 90% of our Android applications. We use either of Threads, AsyncTasks, IntentServices etc to access data from webservices. Each of the ways has its own pros&cons. I’m going to show you my way to do so without using any of above ways. And as far as I know, I can say that this is THE BEST way to access data from web if you can convert your response data to a cursor object.

Assume you need to access the below webservice and display user name and id in a ListView as in screenshot. https://api.github.com/gists/public



MainActivity.java :

import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.support.v4.widget.SimpleCursorAdapter;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.Toast;

public class MainActivity extends FragmentActivity implements OnItemClickListener, LoaderCallbacks<Cursor> {

private static final String WEB_URL = "https://api.github.com/gists/public";
private static final int LOADER_ID = 1234;
private ListView listView;
private SimpleCursorAdapter adapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 listView = (ListView)findViewById(R.id.listView);
 String from[] = {"login", "id", "login"};
 int to[] = {R.id.loginTextView, R.id.idTextView, R.id.listRowContainer};
 adapter = new SimpleCursorAdapter(this, R.layout.list_row, null, from, to, 0);
adapter.setViewBinder(new CustomViewBinder(this));
 //Comment the above line if You don't need to customize any view dynamically in ListView rows. 
 //If you don't comment the above line, The column names can be in any order in 'from' array.
 //You can repeat column names If you don't have enough columns in your cursor.
 //And Specify all the ids of UI elements in 'to' array which you want to change dynamically
 //If you comment the above line, 'to' array should contain the ids of TextViews or Buttons only.

 listView.setAdapter(adapter);
 listView.setOnItemClickListener(this);

 Bundle queryBundle = new Bundle();
 queryBundle.putString("url", WEB_URL);

 getSupportLoaderManager().initLoader(LOADER_ID, queryBundle, this);

}

@Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle queryBundle) {
 return new DownLoader(this, queryBundle.getString("url"));
}

@Override
public void onLoadFinished(final Loader<Cursor> loader, final Cursor responseCursor) {
if(loader.getId() == LOADER_ID && responseCursor != null){
 adapter.swapCursor(responseCursor);
 //If you want to implement Load more feature to your listview
 //then you can merge the new response cursor with old cursor and swap it to adapter.
      }
}

@Override
public void onLoaderReset(Loader<Cursor> loader) {
 adapter.swapCursor(null);
}

@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
 Cursor cursor = ((SimpleCursorAdapter)listView.getAdapter()).getCursor();
 Toast.makeText(getApplicationContext(), cursor.getString(cursor.getColumnIndexOrThrow("login"))+", "+cursor.getString(cursor.getColumnIndexOrThrow("id")),  Toast.LENGTH_LONG).show();
}

}

 DownLoader.java :

import android.content.Context;
import android.database.Cursor;
import android.support.v4.content.CursorLoader;

public class DownLoader extends CursorLoader {

      private String url;
      public DownLoader(Context context, String url) {
            super(context);
            this.url = url;
      }
      @Override
      public Cursor loadInBackground() {
      return new WebServiceStub().getCursorFromWebResponse(url);
      }
}

WebServiceStub.java :
import java.util.ArrayList;
import java.util.List;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import android.database.Cursor;
import android.database.MatrixCursor;

public class WebServiceStub {

//This is the key method which converts the response string as required cursor object.
public Cursor getCursorFromWebResponse(String url) {
String response = getResponseAsString(getWebResponse(url));
MatrixCursor emptyCursor = new MatrixCursor(new String[0]);
MatrixCursor resultCursor;
String keysArray[] = new String[]{"_id", "login", "id"};
////Added extra "_id" column as adapters need it for traversal
try {
JSONArray mainJsonArray = new JSONArray(response);
//Create a new Cursor with JSON keys as columns
resultCursor = new MatrixCursor(keysArray, mainJsonArray.length());
for (int i = 0; i < mainJsonArray.length(); i++) {
JSONObject singleJsonObject = mainJsonArray.getJSONObject(i);
JSONObject userJsonObject = singleJsonObject.getJSONObject("user");
List<Object> values = new ArrayList<Object>();
values.add(i);
values.add(userJsonObject.getString(keysArray[1]));
values.add(userJsonObject.getString(keysArray[2]));
resultCursor.addRow(values);
}
return resultCursor;
} catch (Exception e) {
e.printStackTrace();
}
return emptyCursor;
}

private HttpEntity getWebResponse(String url) {
try {
return new DefaultHttpClient().execute(new HttpGet(url)).getEntity();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private String getResponseAsString(HttpEntity entity){
try {
return EntityUtils.toString(entity);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

CustomViewBinder.java

import android.app.Activity;
import android.database.Cursor;
import android.graphics.Color;
import android.support.v4.widget.SimpleCursorAdapter.ViewBinder;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;

public class CustomViewBinder implements ViewBinder {

int i;
public CustomViewBinder(Activity act) {
}

@Override
public boolean setViewValue(View view, Cursor cursor, int pos) {
int id = view.getId();
if(id == R.id.loginTextView){
 ((TextView)view).setText(cursor.getString(cursor.getColumnIndexOrThrow("login")));
 ((TextView)view).setTextColor(Color.BLUE);
}else if(id == R.id.idTextView){
 ((TextView)view).setText(cursor.getString(cursor.getColumnIndexOrThrow("id")));
 ((TextView)view).setTextColor(Color.RED);
}else if(id == R.id.listRowContainer){
 ((LinearLayout)view).setBackgroundColor((pos+i++)%2 == 0 ? Color.CYAN : Color.WHITE);
 }
 return true;
}

}

Main.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:background="@android:color/white"
    tools:context=".MainActivity" >

    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:cacheColorHint="#00000000" />

</RelativeLayout>

List_row.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/listRowContainer"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:padding="10dp" >

    <TextView
        android:id="@+id/loginTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:ellipsize="end"
        android:maxLines="1"
        android:singleLine="true"
        android:textColor="@android:color/black"
        android:textSize="17sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/idTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:ellipsize="end"
        android:maxLines="1"
        android:singleLine="true"
        android:textColor="@android:color/black"
        android:textSize="17sp"
        android:textStyle="bold" />

</LinearLayout>

NOTE : Don’t forget adding INTERNET permission and Min Required sdk = 8 in your manifest.xml and support library v4 jar file must be available in libs folder. If your min sdk is above or equal to 11 and not using support library jar then you should use getLoaderManager() instead of getSupportLoaderManager() and change required import statements in MainActvity.java.

Why is this THE BEST ?
You don’t need to take care of below things :
1.      Configuration/Orientation changes.
2.      Handling Threads, Handlers, Synchronization with UI thread.
3.      UI is responsive all the time.

And ofcource this way gives less possible errors. Do you need anything extra?



1 comment: