ListView like Android Market Application with separator and ColdFusion as web server


Hi folks. This time I’m presenting a huge post.

This post will cover following things:

  • ListView with separator
  • Pagination  like Android Market application

I know that not all of you knows ColdFusion and its component (cfc), but it is just a server part like your web service code.

myWebservice.cfc

<cfcomponent displayname="myWebservice">
	<cffunction name="getJobs" access="remote" output="false" returntype="struct">
		<cfargument name="currentPage" required="true" type="numeric" default='1' />
		<cfargument name="totalRecord" required="true" type="numeric" default="10" />
		<cfargument name="searchString" required="false" type="string" default="" />
		<cfset var qResult = "">
		<cfset var condition = "" />
		<cfset var startRow = (arguments.CurrentPage-1) * arguments.TotalRecord +1 />
		<cfset var endRow = startRow + arguments.TotalRecord -1 />
		<cfset var qGetAllRecord = "" />
		<cfset var gridStruct = "" />
		<cfif len(arguments.searchString)>
			<cfset condition = condition & " -- your search conditions goes here" />
		</cfif>
		<cfstoredproc procedure="usp_Pagination" datasource="#application.DSN#" username="#application.DBUID#" password="#application.DBPWD#">
			<cfprocresult name="qGetAllRecord" resultset="1">
			<cfprocresult name="qtotalRecord" resultset="2">
			<cfprocparam dbvarname="@SqlColumns" cfsqltype="CF_SQL_VARCHAR" value="*">
			<cfprocparam dbvarname="@SqlFriendlyColumns" cfsqltype="CF_SQL_VARCHAR" value="*">
			<cfprocparam dbvarname="@SqlTableClause" cfsqltype="CF_SQL_VARCHAR" value="Jobs">
			<cfprocparam dbvarname="@StartRow" cfsqltype="CF_SQL_VARCHAR" value="#startRow#">
			<cfprocparam dbvarname="@EndRow" cfsqltype="CF_SQL_VARCHAR" value="#endRow#">
			<cfprocparam dbvarname="@SqlWhere" cfsqltype="CF_SQL_VARCHAR" value="#condition#">
			<cfprocparam dbvarname="@SqlRowNumOrderBy" cfsqltype="CF_SQL_VARCHAR" value="Jobs.createdDate desc">
			<cfprocparam dbvarname="@SqlOuterOrderBy" cfsqltype="CF_SQL_VARCHAR" value="createdDate desc">
		</cfstoredproc>
		<cfset gridStruct=StructNew() />
		<cfset gridStruct.query = qGetAllRecord />
		<cfset gridStruct.totalRowCount=qtotalRecord.countAll />
		<cfreturn qResult>
	</cffunction>
</cfcomponent>

The reason behind showing this code is to let you know the implementation of the server part.

In above function, I am returning fix number of records from my database. It is used to pull out ten ten records from a query that has thousands of records.

Now I’m calling this web service like:

http://localhost/cfc/mywebservice.cfc?returnformat=json&method=getJobs&currentPage=5&totalRecord=10

And it returns a data in JSON format like:

{“QUERY”:{“COLUMNS”:[“JOBID”,”JOBTITLE”,”CREATEDATE”,”JOBDETAIL”],”DATA”:[[“1″,”Title1″,”January, 25 2011 10:55:53″,”Detail is here”],[“2″,”Title2″,”January, 25 2011 10:55:53″,”Detail is here”],[“3″,”Title3″,”January, 20 2011 14:13:10″,”Detail is here”],[“4″,”Title4″,”January, 20 2011 14:13:10″,”Detail is here”],[“5″,”Title5″,”January, 20 2011 14:13:10″,”Detail is here”],[“6″,”Title6″,”January, 20 2011 14:13:10″,”Detail is here”],[“7″,”Title7″,”January, 18 2011 16:23:28″,”Detail is here”],[“8″,”Title8″,”January, 17 2011 11:22:02″,”Detail is here”],[“9″,”Title9″,”January, 11 2011 09:22:32″,”Detail is here”],[“10″,”Title10″,”January, 11 2011 09:22:32″,”Detail is here”]]},”TOTALROWCOUNT”:”120″}

Now we are ready to build Android app.

joblist.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 	android:layout_width="fill_parent"
 	android:layout_height="fill_parent"
	android:orientation="vertical">
 <EditText android:id="@+id/JobSearch"
 		android:layout_height="wrap_content"
		android:layout_width="fill_parent"
		android:singleLine="true"
 		android:hint="Search"/>
	<ListView android:id="@+id/JobList"
 		android:layout_width="fill_parent"
		android:layout_height="wrap_content" />
	<TextView android:id="@+id/EmptyJobList" android:layout_width="fill_parent"
		android:layout_height="fill_parent" android:text="No Results" android:visibility="invisible" />
</LinearLayout>

joblistheader.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical" android:layout_width="fill_parent"
	android:layout_height="wrap_content">
	<TextView android:id="@+id/JobDate" android:layout_width="fill_parent"
		android:layout_height="wrap_content" android:gravity="center"
		style="?android:attr/listSeparatorTextViewStyle" />
</LinearLayout>

joblistitem.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical" android:layout_width="fill_parent"
	android:layout_height="wrap_content" android:paddingRight="5px">
	<TextView android:id="@+id/JobTitle" android:layout_width="wrap_content"
		android:layout_height="wrap_content" android:layout_alignParentLeft="true"
		android:text="jobTitle" style="?android:attr/textAppearanceLarge" />
	<TextView android:id="@+id/JobDetail"
		android:layout_width="fill_parent" android:layout_height="wrap_content" />
</LinearLayout>

And MyJobListView.java

package com.isummation.listview;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.TreeSet;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.json.JSONArray;
import org.json.JSONObject;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.BaseAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

public class MyListView extends Activity {
	private static boolean isInitialized;
	private static int JOBID_IDX;
	private static int JOBTITLE_IDX;
	private static int CREATEDDATE_IDX;
	private static int JOBDETAIL_IDX;
	private JobListAdapter jobListAdapter;
	private long TotalRowCount;
	private int totalRecordPerCall = 20;
	private Date lastDate;
	private EditText JobSearch;
	private ListView list;

	class JobListRowData {
		boolean isHeader;
		Date jobCreationDate;
		int jobId;
		String jobTitle;
		String jobDetail;
	}

	class JobListAdapter extends BaseAdapter {
		private static final int TYPE_ITEM = 0;
		private static final int TYPE_SEPARATOR = 1;
		private static final int TYPE_MAX_COUNT = TYPE_SEPARATOR + 1;
		private LayoutInflater mInflater;
		private ArrayList mData = new ArrayList();
		private TreeSet mSeparatorsSet = new TreeSet();
		private int count = 0;
		private boolean isCleared = true;

		public JobListAdapter() {
			mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
			initialize();
		}

		public void ParseJSONArray(JSONArray JQueryData) {
			int len = JQueryData.length();

			final TextView EmptyListView = (TextView) findViewById(R.id.EmptyJobList);

			if (TotalRowCount == 0) {
				EmptyListView.setVisibility(View.VISIBLE);
			} else {
				EmptyListView.setVisibility(View.INVISIBLE);
			}
			try {
				for (int i = 0; i &lt; len; i++) {

					Date tempDate = new Date(JQueryData.getJSONArray(i)
					.getString(CREATEDDATE_IDX));

					if (lastDate == null) {
						JobListRowData jobListRowData = new JobListRowData();
						jobListRowData.isHeader = true;
						jobListRowData.jobCreationDate = new Date(JQueryData.getJSONArray(i)
								.getString(CREATEDDATE_IDX));
						addSeparatorItem(jobListRowData);

						jobListRowData = new JobListRowData();
						jobListRowData.jobId = JQueryData.getJSONArray(i)
								.getInt(JOBID_IDX);
						jobListRowData.jobTitle = JQueryData.getJSONArray(i)
								.getString(JOBTITLE_IDX);
						jobListRowData.jobDetail = JQueryData.getJSONArray(i)
								.getString(JOBDETAIL_IDX);
						addItem(jobListRowData);

						lastDate = new Date(JQueryData.getJSONArray(i)
								.getString(CREATEDDATE_IDX));
					} else if (lastDate.getDate() == tempDate.getDate()
							&amp;&amp; lastDate.getMonth() == tempDate.getMonth()
							&amp;&amp; lastDate.getYear() == tempDate.getYear()) {
						JobListRowData jobListRowData = new JobListRowData();
						jobListRowData.jobId = JQueryData.getJSONArray(i)
								.getInt(JOBID_IDX);
						jobListRowData.jobTitle = JQueryData.getJSONArray(i)
								.getString(JOBTITLE_IDX);
						jobListRowData.jobDetail = JQueryData.getJSONArray(i)
								.getString(JOBDETAIL_IDX);
						addItem(jobListRowData);
					} else {
						JobListRowData jobListRowData = new JobListRowData();
						jobListRowData.isHeader = true;
						jobListRowData.jobCreationDate = new Date(JQueryData.getJSONArray(i)
								.getString(CREATEDDATE_IDX));
						addSeparatorItem(jobListRowData);
						lastDate = new Date(JQueryData.getJSONArray(i)
								.getString(CREATEDDATE_IDX));

						jobListRowData = new JobListRowData();
						jobListRowData.jobId = JQueryData.getJSONArray(i)
								.getInt(JOBID_IDX);
						jobListRowData.jobTitle = JQueryData.getJSONArray(i)
								.getString(JOBTITLE_IDX);
						jobListRowData.jobDetail = JQueryData.getJSONArray(i)
								.getString(JOBDETAIL_IDX);
						addItem(jobListRowData);
					}

				}
			} catch (Exception e) {
				Toast.makeText(getApplicationContext(), e.getMessage(),
						Toast.LENGTH_LONG).show();
				Log.e(e.getClass().getName(), e.getMessage(), e);
				TotalRowCount = 0;
			}
		}

		public void initialize() {
			isCleared = false;
			count = 0;
			lastDate = null;
			mSeparatorsSet.clear();
			mData.clear();
			JSONArray JQueryData = getJobs(1);
			ParseJSONArray(JQueryData);
			isCleared = true;
		}

		public void addMoreData() {
			if (jobListAdapter.count &lt; TotalRowCount &amp;&amp; isCleared) { 
				JSONArray JQueryData = getJobs((jobListAdapter.count + totalRecordPerCall)
						/ totalRecordPerCall + 1);
				ParseJSONArray(JQueryData);
			}
		}

		public void addItem(final JobListRowData item) {
			mData.add(item);
			count += 1;
			notifyDataSetChanged();
		}

		public void addSeparatorItem(final JobListRowData item) {
			mData.add(item);
			mSeparatorsSet.add(mData.size() - 1);
			notifyDataSetChanged();
		}

		public int getItemViewType(int position) {
			return mSeparatorsSet.contains(position) ? TYPE_SEPARATOR
					: TYPE_ITEM;
		}

		public int getViewTypeCount() {
			return TYPE_MAX_COUNT;
		}

		public int getCount() {
			return mData.size();
		}

		public JobListRowData getItem(int pos) {
			return mData.get(pos);
		}

		public long getItemId(int pos) {
			return pos;
		}

		@Override
		public boolean isEnabled(int position) {
			return mSeparatorsSet.contains(position) ? false : true;
		}

		public View getView(int position, View convertView, ViewGroup parent) {
			ViewHolder holder;
			int type = getItemViewType(position);
			if (convertView == null) {
				holder = new ViewHolder();
				switch (type) {
				case TYPE_ITEM:
					convertView = mInflater.inflate(R.layout.joblistitem, null);
					holder.text = (TextView) convertView
							.findViewById(R.id.JobTitle);
					holder.text2 = (TextView) convertView
							.findViewById(R.id.JobDetail);
					break;
				case TYPE_SEPARATOR:
					convertView = mInflater.inflate(R.layout.joblistheader,
							null);
					holder.text = (TextView) convertView
							.findViewById(R.id.JobDate);
					break;
				}
				convertView.setTag(holder);
			} else {
				holder = (ViewHolder) convertView.getTag();
			}
			switch (type) {
			case TYPE_ITEM:
				holder.text.setText(mData.get(position).jobTitle);
				holder.text2.setText(mData.get(position).jobDetail);
				break;
			case TYPE_SEPARATOR:
				try {
					holder.text
							.setText(android.text.format.DateFormat.format(
									"MMM dd, yyyy",
									mData.get(position).jobCreationDate));
				} catch (Exception e) {
					Toast.makeText(getApplicationContext(), e.getMessage(),
							Toast.LENGTH_LONG).show();
					Log.e(e.getClass().getName(), e.getMessage(), e);
				}
				break;
			}
			return convertView;
		}

		class ViewHolder {
			TextView text;
			TextView text2;
		}
	}

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

		JobSearch = (EditText) findViewById(R.id.JobSearch);
		JobSearch.setImeOptions(EditorInfo.IME_ACTION_SEARCH);
		JobSearch.setOnKeyListener(new View.OnKeyListener() {

			public boolean onKey(View v, int keyCode, KeyEvent event) {
				if (keyCode == KeyEvent.KEYCODE_ENTER
						&amp;&amp; event.getAction() == KeyEvent.ACTION_DOWN) {
					jobListAdapter.initialize();
				}
				return false;
			}
		});

		jobListAdapter = new JobListAdapter();

		list = (ListView) findViewById(R.id.JobList);
		list.setAdapter(jobListAdapter);
		list.setOnScrollListener(new AbsListView.OnScrollListener() {

			public void onScrollStateChanged(AbsListView view, int scrollState) {

			}

			public void onScroll(AbsListView view, int firstVisibleItem,
					int visibleItemCount, int totalItemCount) {
				boolean loadMore = firstVisibleItem + visibleItemCount &gt;= totalItemCount;
				if (loadMore) {
					jobListAdapter.addMoreData();
				}
			}
		});

		list.setOnItemClickListener(new OnItemClickListener() {
			public void onItemClick(AdapterView<!--?--> parent, View view,
					int position, long id) {
				JobListRowData item = jobListAdapter.getItem(position);
				if (!item.isHeader) {
					// start another activity
				}
			}
		});
	}

	public JSONArray getJobs(int currentPage) {
		try {
			String searchString = JobSearch.getText().toString();
			HttpClient httpClient = new DefaultHttpClient();
			HttpContext localContext = new BasicHttpContext();
			HttpGet httpGet = new HttpGet(
					"http://10.0.2.2/cfc/iphonewebservice.cfc?returnformat=json&amp;method=getJobs¤tPage="
							+ URLEncoder.encode("" + currentPage, "UTF-8")
							+ "&amp;totalRecord="
							+ totalRecordPerCall
							+ "&amp;searchString="
							+ URLEncoder.encode(searchString, "UTF-8"));
			HttpResponse response = httpClient.execute(httpGet, localContext);

			BufferedReader reader = new BufferedReader(new InputStreamReader(
					response.getEntity().getContent(), "UTF-8"));
			String sResponse = reader.readLine();
			JSONObject JResponse = new JSONObject(sResponse);
			JSONObject JQuery = JResponse.getJSONObject("QUERY");
			TotalRowCount = JResponse.getLong("TOTALROWCOUNT");
			JSONArray JQueryColumns = JQuery.getJSONArray("COLUMNS");

			if (!isInitialized) {
				int len = JQueryColumns.length();
				for (int i = 0; i &lt; len; i++) {
					if (JQueryColumns.getString(i).equals("JOBID")) {
						JOBID_IDX = i;
					} else if (JQueryColumns.getString(i).equals("JOBTITLE")) {
						JOBTITLE_IDX = i;
					} else if (JQueryColumns.getString(i).equals("CREATEDATE")) {
						CREATEDDATE_IDX = i;
					} else if (JQueryColumns.getString(i).equals(
							"JOBDETAIL")) {
						JOBDETAIL_IDX = i;
					}
				}
				isInitialized = true;
			}

			return JQuery.getJSONArray("DATA");
		} catch (Exception e) {
			Toast.makeText(getApplicationContext(), e.getMessage(),
					Toast.LENGTH_LONG).show();
			Log.e(e.getClass().getName(), e.getMessage(), e);
			finish();
			return new JSONArray();
		}
	}
}

Update:

I’ve updated above code, for “Loading…” effect while getting next 20 records.
You can now download the source code.
Download source code