Thursday, June 27, 2013

Sorting a list of objects

When developing an application a common task that has to be executed very often is sorting a list objects by a specific attribute of these objects. In this post I am gonna show you how to sort a list of objects in ascending or descending order based on the selection that the user has made.

Let's say that we have a shopping list. Each item on the list has a name, a price and a quantity number. We will create an application that sorts the list by name, by price and by quantity, in ascending or descending order, depending on the selection of the user. To accomplish that we will use Collections.sort() method and Comparator class.

First of all we need to create a class that holds the information of each of our items in the shopping list. The class ShoppingListItem is shown below:
package com.androidsnippet.sortlist;

public class ShoppingListItem {

private String name;
private double price;
private int quantity;

/**
* @return the name of the item
*/
public String getName() {
return name;
}

/**
* Set the name of the item
*
* @param name The name of the item
*/
public void setName(String name) {
this.name = name;
}

/**
* @return the price of the item
*/
public double getPrice() {
return price;
}

/**
* Set the price of the item
*
* @param price The price of the item
*/
public void setPrice(double price) {
this.price = price;
}

/**
* @return the quantity of the item
*/
public int getQuantity() {
return quantity;
}

/**
* Set the quantity of the item
*
* @param quantity The quantity of the item
*/
public void setQuantity(int quantity) {
this.quantity = quantity;
}
}
Nothing special here, just a simple class that consists of get and set functions. Now let's define the layout of our Activity. We need to have three TextViews (name, price, quantity), an ImageView (up or down arrow) next to each these TextViews showing the ascending or descending order of the sorting of the list and a ListView that contains our items on the shopping list. Our main.xml file is shown below:
<?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="fill_parent">

<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:baselineAligned="false">

<LinearLayout
android:id="@+id/name_layout"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_vertical"
android:paddingLeft="2dp"
android:layout_weight="1">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="2dp"
android:text="@string/name"/>
<ImageView
android:id="@+id/name_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="2dp"
android:src="@drawable/arrowdown"/>
</LinearLayout>

<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
android:baselineAligned="false">

<LinearLayout
android:id="@+id/price_layout"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_vertical|center"
android:layout_weight="1">

<TextView
android:id="@+id/price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/price"/>

<ImageView
android:id="@+id/price_arrow"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingLeft="2dp"
android:src="@drawable/arrowdown"
android:visibility="gone"/>
</LinearLayout>

<LinearLayout
android:id="@+id/quantity_layout"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_vertical|center"
android:layout_weight="1">

<TextView
android:id="@+id/quantity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/quantity"/>

<ImageView
android:id="@+id/quantity_arrow"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingLeft="2dp"
android:src="@drawable/arrowdown"
android:visibility="gone"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>

<ListView android:id="@+id/android:list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
<TextView android:id="@+id/android:empty"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:text="@string/no_items"/>
</LinearLayout>
Each row of our ListView will be composed of  a custom layout. So, we also need to define this layout for each row. Again, we need to have three TextViews that represent the name, the price and the quantity of each item. The list_row.xml is quoted below:
<?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:paddingTop="5dp"
android:paddingBottom="5dp">

<TextView
android:id="@+id/item_name"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_vertical"
android:paddingLeft="2dp"
android:layout_weight="1"/>

<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1">

<TextView
android:id="@+id/item_price"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
android:gravity="center"/>

<TextView
android:id="@+id/item_quantity"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
android:gravity="center"/>

</LinearLayout>
</LinearLayout>
Finally, I am quoting the entire code of the Activity and then I am going to get into details for each function separately. So here is the code of the Activity:
package com.androidsnippet.sortlist;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

import android.app.ListActivity;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.widget.TextView;

public class SortListActivity extends ListActivity {

private LinearLayout mItemName;
private LinearLayout mItemPrice;
private LinearLayout mItemQuantity;
private ImageView mNameArrow;
private ImageView mPriceArrow;
private ImageView mQuantityArrow;
private boolean mAscendingOrder[] = {true, true, true};
private ItemsListAdapter mAdapter;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

mItemName = (LinearLayout) findViewById(R.id.name_layout);
mItemPrice = (LinearLayout) findViewById(R.id.price_layout);
mItemQuantity = (LinearLayout) findViewById(R.id.quantity_layout);
mNameArrow = (ImageView) findViewById(R.id.name_arrow);
mPriceArrow = (ImageView) findViewById(R.id.price_arrow);
mQuantityArrow = (ImageView) findViewById(R.id.quantity_arrow);

mAdapter = new ItemsListAdapter();
setListAdapter(mAdapter);

mItemName.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
// Change the order of items based on name
if(mAscendingOrder[0]) {
// Show items descending
mAscendingOrder[0] = false;
mAdapter.sortByNameDesc();
mNameArrow.setImageResource(R.drawable.arrowup);
}
else {
// Show items ascending
mAscendingOrder[0] = true;
mAdapter.sortByNameAsc();
mNameArrow.setImageResource(R.drawable.arrowdown);
}
// Make visible the arrow next to the name and make the others invisible
mNameArrow.setVisibility(View.VISIBLE);
mPriceArrow.setVisibility(View.GONE);
mPriceArrow.setLayoutParams(new LayoutParams(0,LayoutParams.WRAP_CONTENT));
mQuantityArrow.setVisibility(View.GONE);
mQuantityArrow.setLayoutParams(new LayoutParams(0,LayoutParams.WRAP_CONTENT));
mAscendingOrder[1] = true;
mAscendingOrder[2] = true;
}
});

mItemPrice.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
// Change the order of items based on price
if(mAscendingOrder[1]) {
// Show items descending
mAscendingOrder[1] = false;
mAdapter.sortByPriceDesc();
mPriceArrow.setImageResource(R.drawable.arrowup);
}
else {
// Show items ascending
mAscendingOrder[1] = true;
mAdapter.sortByPriceAsc();
mPriceArrow.setImageResource(R.drawable.arrowdown);
}
// Make visible the arrow next to the price and make the others invisible
mPriceArrow.setVisibility(View.VISIBLE);
mPriceArrow.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT));
mNameArrow.setVisibility(View.GONE);
mQuantityArrow.setVisibility(View.GONE);
mQuantityArrow.setLayoutParams(new LayoutParams(0,LayoutParams.WRAP_CONTENT));
mAscendingOrder[0] = false;
mAscendingOrder[2] = true;
}
});

mItemQuantity.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
// Change the order of items based on quantity
if(mAscendingOrder[2]) {
// Show items descending
mAscendingOrder[2] = false;
mAdapter.sortByQuantityDesc();
mQuantityArrow.setImageResource(R.drawable.arrowup);
}
else {
// Show items ascending
mAscendingOrder[2] = true;
mAdapter.sortByQuantityAsc();
mQuantityArrow.setImageResource(R.drawable.arrowdown);
}
// Make visible the arrow next to the quantity and make the others invisible
mQuantityArrow.setVisibility(View.VISIBLE);
mQuantityArrow.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT));
mNameArrow.setVisibility(View.GONE);
mPriceArrow.setVisibility(View.GONE);
mPriceArrow.setLayoutParams(new LayoutParams(0,LayoutParams.WRAP_CONTENT));
mAscendingOrder[0] = false;
mAscendingOrder[1] = true;
}
});

fillShoppingList();
}

/**
* Fill shopping list with items
*/
private void fillShoppingList() {
ShoppingListItem item = new ShoppingListItem();
item.setName("Milk");
item.setPrice(1.4);
item.setQuantity(2);

mAdapter.addItem(item);

item = new ShoppingListItem();
item.setName("Rice");
item.setPrice(2.5);
item.setQuantity(1);

mAdapter.addItem(item);

item = new ShoppingListItem();
item.setName("Eggs");
item.setPrice(0.5);
item.setQuantity(6);

mAdapter.addItem(item);

item = new ShoppingListItem();
item.setName("Potatoes");
item.setPrice(0.35);
item.setQuantity(10);

mAdapter.addItem(item);
mAdapter.sortByNameAsc();
}

/** Adapter for the shopping list items */
private class ItemsListAdapter extends BaseAdapter {

private LayoutInflater vi;
private ArrayList<ShoppingListItem> shoppingList = new ArrayList<ShoppingListItem>();

public ItemsListAdapter() {
vi = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}

/** Add white line */
public void addItem(ShoppingListItem item) {
shoppingList.add(item);
notifyDataSetChanged();
}

/** Sort shopping list by name ascending */
public void sortByNameAsc() {
Comparator<ShoppingListItem> comparator = new Comparator<ShoppingListItem>() {

@Override
public int compare(ShoppingListItem object1, ShoppingListItem object2) {
return object1.getName().compareToIgnoreCase(object2.getName());
}
};
Collections.sort(shoppingList, comparator);
notifyDataSetChanged();
}

/** Sort shopping list by name descending */
public void sortByNameDesc() {
Comparator<ShoppingListItem> comparator = new Comparator<ShoppingListItem>() {

@Override
public int compare(ShoppingListItem object1, ShoppingListItem object2) {
return object2.getName().compareToIgnoreCase(object1.getName());
}
};
Collections.sort(shoppingList, comparator);
notifyDataSetChanged();
}

/** Sort shopping list by price ascending */
public void sortByPriceAsc() {
Comparator<ShoppingListItem> comparator = new Comparator<ShoppingListItem>() {

@Override
public int compare(ShoppingListItem object1, ShoppingListItem object2) {
return Double.compare(object1.getPrice(), object2.getPrice());
}
};
Collections.sort(shoppingList, comparator);
notifyDataSetChanged();
}

/** Sort shopping list by price descending */
public void sortByPriceDesc() {
Comparator<ShoppingListItem> comparator = new Comparator<ShoppingListItem>() {

@Override
public int compare(ShoppingListItem object1, ShoppingListItem object2) {
return Double.compare(object2.getPrice(), object1.getPrice());
}
};
Collections.sort(shoppingList, comparator);
notifyDataSetChanged();
}

/** Sort shopping list by quantity ascending */
public void sortByQuantityAsc() {
Comparator<ShoppingListItem> comparator = new Comparator<ShoppingListItem>() {

@Override
public int compare(ShoppingListItem object1, ShoppingListItem object2) {
return ((Integer) object1.getQuantity()).compareTo((Integer) object2.getQuantity());
}
};
Collections.sort(shoppingList, comparator);
notifyDataSetChanged();
}

/** Sort shopping list by quantity descending */
public void sortByQuantityDesc() {
Comparator<ShoppingListItem> comparator = new Comparator<ShoppingListItem>() {

@Override
public int compare(ShoppingListItem object1, ShoppingListItem object2) {
return ((Integer) object2.getQuantity()).compareTo((Integer) object1.getQuantity());
}
};
Collections.sort(shoppingList, comparator);
notifyDataSetChanged();
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
holder = new ViewHolder();
convertView = vi.inflate(R.layout.list_row, null);

holder.name = (TextView) convertView.findViewById(R.id.item_name);
holder.price = (TextView) convertView.findViewById(R.id.item_price);
holder.quantity = (TextView) convertView.findViewById(R.id.item_quantity);

convertView.setTag(holder);
}
else {
holder = (ViewHolder) convertView.getTag();
}
// Fill views with data
if(holder.name != null) {
holder.name.setText(shoppingList.get(position).getName());
}
if(holder.price != null) {
holder.price.setText(String.valueOf(shoppingList.get(position).getPrice()));
}
if(holder.quantity != null) {
holder.quantity.setText(String.valueOf(shoppingList.get(position).getQuantity()));
}

return convertView;
}

@Override
public int getCount() {
// TODO Auto-generated method stub
return shoppingList.size();
}

@Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return shoppingList.get(position);
}

@Override
public long getItemId(int position) {
// TODO Auto-generated method stub
return position;
}

/** Helper class acting as a holder of the information for each row */
private class ViewHolder {
public TextView name;
public TextView price;
public TextView quantity;
}
}
}
First of all, take a look at ItemsListAdapter. We are creating a custom class that extends the BaseAdapter class. ItemsListAdapter is the adapter that we are going to bind our ListView with. Function addItem() is pretty self explanatory. It just adds one item to the list of the adapter. Function sortByNameAsc() sorts the list by name in ascending order. Function sortByNameAsc() sorts the list by name in descending order. Same things apply for price and quantity ( sortByPriceAsc(), sortByPriceDesc(), sortByQuantityAsc(), sortByQuantityDesc() ). As stated above sorting is achieved through Collections.sort() method and Comparator class. Finally, we override getView function of the adapter in order to inflate the layout of this view and fill its fields with information.

On onCreate() function we initialize our fields and fill the list with items. We have three layouts and we apply a listener for each one of them. Each time we click a layout we check its current order, we reverse it and sort the list again. We hold the information of the sorting order of each layout in mAscendingOrder[] array. For example, if we click mItemName layout we check the respective position in mAscendingOrder[] array. If it ascending, then we change it to descending and sort the list by name in descending order. Finally, fillShoppingList() function fills the list with items.

Now let's take a short look of what we have accomplished. The first image shows our list sorted by name in ascending order. This is the default setting when we launch our application.
If we then click on Price our list gets sorted by price in descending order.
So here ends the second tutorial. You can find the whole project here! Feel free to leave a comment below! 

Cheers!