Save memory with view recycling. ListView plus custom ArrayAdapter. Dubai Tour Guide #6.

When you’re creating an app almost always you happen to use a vertically scrolling list. It is also often the case that the list has a lot of records that can grow even to 100 or more positions. For example you might have a list of words with its translations to display if you’re building a language learning app. Or as it is in my case, I have a list of places worth visiting in Dubai that can get really big with time. In either case you should be keen on saving as much of memory as you can. When it comes to Android the view recycling appears to be very handy. It works in a very simple way. Say you have 100 records in your list view. The user’s screen fits 7 only and only those 7 are loaded. While a user scrolls, the view (our list record) that comes out of the screen is not destroyed but kept in memory to be reused to populate with the new content that enters the screen. Thanks to this, even though you have 100 views on your list, only 7 views are instantiated and held in memory. You can learn more about this concept from this Udacity video.

App layout overview

I will show you how to create a ListView with a custom ArrayAdapter using a recycling view. I will also use a layout that consists of fragments. Please see the below design, so you can better imagine the purpose of this exercise:

Dubai Tour Guide App — Home Screen

As you can see above, we have a tab layout that a user can swipe through. Categories: General, Places, Food, Nature are built using fragments. The list items for General category would be: Weather, Dress Code, Alcohol, Ramadan etc. (Ramadan already falls out of the screen).

Creating a custom class “Category”

An ArrayAdapter can handle only one TextView widget inside the layout of the View objects it generates. We have an additional ImageView to fit the image. That is why we need to extend ArrayAdapter . But first let’s create a custom class. You can name your class whatever suits your project best. In my case it just happen to be Category. Here is the example code what you would need to create:

public class Category {

// Name of category, e.g. Weather, Dress Code
private String mCategoryName;

// Drawable resource ID
private int mImageResourceId;


/**
* Create a new Category object.
*
*
@param cName is the name of the Category, e.g. Burj Khalifa, SeaView Restaurant
*
@param imageResourceId is the drawable resource ID for the image associated with the category name
*
*/
public Category(String cName, int imageResourceId) {
mCategoryName = cName;
mImageResourceId = imageResourceId;
}

/**
* Get the name of category
*/
public String getCategoryName() {

return mCategoryName;
}

/**
* Get the image resource ID
*/
public int getImageResourceId() {
return mImageResourceId;
}

}

Creating CategoryAdapter

Once we have the Category class ready, the next step would be to create a CategoryAdapter which is an ArrayAdapter that can provide the layout for each list, based on a data source, which is a list of Category objects. Uhhh! That was quite a long sentence. See the example below, also for better understanding, please pay attention to comments within the code.

public class CategoryAdapter extends ArrayAdapter<Category> {

/**
* This is our own custom constructor (it doesn't mirror a superclass constructor).
* The context is used to inflate the layout file, and the list is the data we want
* to populate into the lists.
*
*
@param context The current context. Used to inflate the layout file.
*
@param categories A List of Category objects to display in a list
*/
public CategoryAdapter(Activity context, ArrayList<Category> categories) {
// Here, we initialize the ArrayAdapter's internal storage for the context and the list.
// the second argument is used when the ArrayAdapter is populating a single TextView.
// Because this is a custom adapter for one TextView and an ImageView, the adapter is not
// going to use this second argument, so it can be any value. Here, we used 0.
super(context, 0, categories);
}

/**
* Provides a view for an AdapterView (ListView, GridView, etc.)
*
*
@param position The position in the list of data that should be displayed in the
* list item view.
*
@param convertView The recycled view to populate.
*
@param parent The parent ViewGroup that is used for inflation.
*
@return The View for the position in the AdapterView.
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// Check if the existing view is being reused, otherwise inflate the view
View listItemView = convertView;
if (listItemView == null) {
listItemView = LayoutInflater.from(getContext()).inflate(
R.layout.category_list_item, parent, false);
}

// Get the {@link Category} object located at this position in the list
Category currentCategory = getItem(position);

// Find the TextView in the category_list_item.xml layout with the ID category_name
TextView nameTextView = (TextView) listItemView.findViewById(R.id.category_name);
// Get the category name from the current Category object and
// set this text on the nameTextView
nameTextView.setText(currentCategory.getCategoryName());

// Find the ImageView in the category_list_item.xml layout with the ID category_image
ImageView imageView = (ImageView) listItemView.findViewById(R.id.category_image);
// Get the image resource ID from the current Category object and
// set the image to imageView
imageView.setImageResource(currentCategory.getImageResourceId());

// Return the whole list item layout (containing 1 TextView and an ImageView)
// so that it can be shown in the ListView
return listItemView;

}

}

Then in each fragment you need to create an ArrayList of Category objects, please see the example code for the General fragment below:

final ArrayList<Category> categories = new ArrayList<Category>();
categories.add(new Category(getString(R.string.wheather), R.drawable.sunset));
categories.add(new Category(getString(R.string.dress_code), R.drawable.dresscode));
categories.add(new Category(getString(R.string.alcohol), R.drawable.alcohol_party_dark));
categories.add(new Category(getString(R.string.ramadan), R.drawable.dubai_at_night));
categories.add(new Category(getString(R.string.alcohol), R.drawable.dubai_at_night));
categories.add(new Category(getString(R.string.prices), R.drawable.dubai_at_night));

The next step is to instantiate the CategoryAdapter and attach the adapter to the ListView.

// Create an {@link CategoryAdapter}, whose data source is a list of
// {@link Categories}. The adapter knows how to create list item views for each item
// in the list.
CategoryAdapter categoryAdapter = new CategoryAdapter(getActivity(), categories);

// Get a reference to the ListView, and attach the adapter to the listView.
ListView listView = (ListView) rootView.findViewById(R.id.list);
listView.setAdapter(categoryAdapter);

Additionally please find below the corresponding XML layouts for the listview and list item.

category_list.xml

ListView is a view which shows items in a vertically scrolling list. To achieve it we need to add ListView in layout.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical">

<ListView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/list"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:drawSelectorOnTop="true"
android:background="#ffffff"
android:dividerHeight="3dp"
android:divider="#ffffff"/>
</LinearLayout>

The above ListView is then accessed in the code to set the adapter:

ListView listView = (ListView) rootView.findViewById(R.id.list);
listView.setAdapter(categoryAdapter);

category_list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">


<ImageView
android:layout_width="match_parent"
android:layout_height="170dp"
android:src="@drawable/dubai_desert"
android:id="@+id/category_image"
android:scaleType="fitXY"/>

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="30sp"
android:text="Desert Experience"
android:fontFamily="sans-serif-light"
android:layout_marginTop="40dp"
android:textAlignment="center"
android:textColor="#ffffff"
android:padding="24dp"
android:id="@+id/category_name" />

</RelativeLayout>

The full code for this project can be found on GitHub. However please bare in mind that there might be additional elements included, as I simplified the code for the purpose of this post.