Ilias Tsagklis

About Ilias Tsagklis

Ilias Tsagklis is a senior software engineer working in the telecom domain. He is an applications developer in a wide variety of applications/services. Ilias is co-founder and Executive Editor at Java Code Geeks.

Android Full App, Part 6: Customized list view for data presentation

This is the sixth part of the “Android Full Application Tutorial” series. The complete application aims to provide an easy way of performing movies/actors searching over the internet. In the first part of the series (“Main Activity UI”), we created the Eclipse project and set up a basic interface for the main activity of the application. In the second part (“Using the HTTP API”), we used the Apache HTTP client library in order to consume an external HTTP API and integrate the API’s searching capabilities into our application. In the third part (“Parsing the XML response”) we saw how to parse the XML response using Android’s built-in XML parsing capabilities. In the fourth part (“Performing the API request asynchronously from the main activity”), we tied together the HTTP retriever and XML parser services in order to perform the API search request from our application’s main activity. The request was executed asynchronously in a background thread in order to avoid blocking the main UI thread. In the fifth part (“Launching new activities with intents”), we saw how to launch a new Activity and how to transfer data from one Activity to another. In this part, we are going to create a custom list view in order to provide a better data visual presentation.

The view used to present the search results in our tutorial is quite rudimentary and plain. If you recall, the results were passed to a ListActivity which, upon rendering, looked like this:

We are now going to spice up the activity’s UI. The first step is to replace the ArrayAdapter we used up to this moment and implement a custom adapter. Our adapter will extend the ArrayAdapter class and override its getView method in order to provide a custom list View.

Remember that the Movie model class contains various information regarding the corresponding movie, among which are the following:

  • Movie rating
  • Release date
  • Certification
  • Language
  • Thumbnail image URL

We are going to create a custom layout that will include the aforementioned data for each of the movies included in the search results. This is an image of what each row of the list will look like:

(Note: Our implementation was based on the great example provided here)

The XML file that describes the layout of each row is named “movie_data_row.xml” and contains the following:

<?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="?android:attr/listPreferredItemHeight"
    android:padding="6dip">
    
    <ImageView
        android:id="@+id/movie_thumb_icon"
        android:layout_width="wrap_content"
        android:layout_height="fill_parent"
        android:layout_marginRight="6dip"/>
        
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="0dip"
        android:layout_weight="1"
        android:layout_height="fill_parent">
        
        <TextView
            android:id="@+id/name_text_view"
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1"
            android:singleLine="true"
            android:ellipsize="marquee"
            android:textStyle="bold"
        />
        
        <TextView
            android:id="@+id/rating_text_view"
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1"
            android:singleLine="true"
            android:ellipsize="marquee"
        />
        
        <TextView
            android:id="@+id/released_text_view"
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1"
            android:singleLine="true"
            android:ellipsize="marquee"
        />
        
        <TextView
            android:id="@+id/certification_text_view"
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1"
            android:singleLine="true"
            android:ellipsize="marquee"
        />
            
        <TextView
            android:id="@+id/language_text_view"
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1"
            android:singleLine="true"
            android:ellipsize="marquee"
        />
          
        <TextView
            android:id="@+id/adult_text_view"
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1"
            android:singleLine="true"
            android:ellipsize="marquee"
        />
                     
    </LinearLayout>
    
</LinearLayout>

We use a LinearLayout for the base layout and inside that we include an ImageView (which will hold the thumbnail image) and another LinearLayout which is a place holder for a number of TextViews. Each element is assigned a unique ID so that it can be later referenced from our adapter.

Notice that an ArrayAdapter cannot use the method setContentView that is typically used by an Activity to declare the layout that will be used. The way to retrieve an XML layout during runtime is by using the LayoutInflater service. This class is used to instantiate layout XML file into its corresponding View objects. More specifically, the inflate method is used in order to inflate a new view hierarchy from the specified xml resource. After we have taken reference of the underlying view, we can use it as usual and modify its internal widgets, i.e. provide the text to the TextViews and load the image in the ImageView. Here is the code for our adapter:

package com.javacodegeeks.android.apps.moviesearchapp.ui;

import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.LinkedHashMap;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

import com.javacodegeeks.android.apps.moviesearchapp.R;
import com.javacodegeeks.android.apps.moviesearchapp.io.FlushedInputStream;
import com.javacodegeeks.android.apps.moviesearchapp.model.Movie;
import com.javacodegeeks.android.apps.moviesearchapp.services.HttpRetriever;

public class MoviesAdapter extends ArrayAdapter<Movie> {
    
    private HttpRetriever httpRetriever = new HttpRetriever();
    
    private ArrayList<Movie> movieDataItems;
    
    private Activity context;
    
    public MoviesAdapter(Activity context, int textViewResourceId, ArrayList<Movie> movieDataItems) {
        super(context, textViewResourceId, movieDataItems);
        this.context = context;
        this.movieDataItems = movieDataItems;
    }
    
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
  
        View view = convertView;
        if (view == null) {
            LayoutInflater vi = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            view = vi.inflate(R.layout.movie_data_row, null);
        }
        
        Movie movie = movieDataItems.get(position);
        
        if (movie != null) {
            
            // name
            TextView nameTextView = (TextView) view.findViewById(R.id.name_text_view);
            nameTextView.setText(movie.name);
            
            // rating
            TextView ratingTextView = (TextView) view.findViewById(R.id.rating_text_view);
            ratingTextView.setText("Rating: " + movie.rating);
            
            // released
            TextView releasedTextView = (TextView) view.findViewById(R.id.released_text_view);
            releasedTextView.setText("Release Date: " + movie.released);
            
            // certification
            TextView certificationTextView = (TextView) view.findViewById(R.id.certification_text_view);
            certificationTextView.setText("Certification: " + movie.certification);
            
            // language
            TextView languageTextView = (TextView) view.findViewById(R.id.language_text_view);
            languageTextView.setText("Language: " + movie.language);
            
            // thumb image
            ImageView imageView = (ImageView) view.findViewById(R.id.movie_thumb_icon);
            String url = movie.retrieveThumbnail();
            
            if (url!=null) {
                Bitmap bitmap = fetchBitmapFromCache(url);
                if (bitmap==null) {                
                    new BitmapDownloaderTask(imageView).execute(url);
                }
                else {
                    imageView.setImageBitmap(bitmap);
                }
            }
            else {
                imageView.setImageBitmap(null);
            }
            
        }
        
        return view;
        
    }
    
    private LinkedHashMap<String, Bitmap> bitmapCache = new LinkedHashMap<String, Bitmap>();
    
    private void addBitmapToCache(String url, Bitmap bitmap) {
        if (bitmap != null) {
            synchronized (bitmapCache) {
                bitmapCache.put(url, bitmap);
            }
        }
    }
    
    private Bitmap fetchBitmapFromCache(String url) {
        
        synchronized (bitmapCache) {
            final Bitmap bitmap = bitmapCache.get(url);
            if (bitmap != null) {
                // Bitmap found in cache
                // Move element to first position, so that it is removed last
                bitmapCache.remove(url);
                bitmapCache.put(url, bitmap);
                return bitmap;
            }
        }

        return null;
        
    }
    
    private class BitmapDownloaderTask extends AsyncTask<String, Void, Bitmap> {
        
        private String url;
        private final WeakReference<ImageView> imageViewReference;

        public BitmapDownloaderTask(ImageView imageView) {
            imageViewReference = new WeakReference<ImageView>(imageView);
        }
        
        @Override
        protected Bitmap doInBackground(String... params) {
            url = params[0];
            InputStream is = httpRetriever.retrieveStream(url);
            if (is==null) {
                  return null;
            }
            return BitmapFactory.decodeStream(new FlushedInputStream(is));
        }
        
        @Override
        protected void onPostExecute(Bitmap bitmap) {            
            if (isCancelled()) {
                bitmap = null;
            }
            
            addBitmapToCache(url, bitmap);

            if (imageViewReference != null) {
                ImageView imageView = imageViewReference.get();
                if (imageView != null) {
                    imageView.setImageBitmap(bitmap);
                }
            }
        }
    }
    
}

Inside our getView method, we first inflate the XML layout file and retrieve reference of the described View. Then, we take reference of each of the views widgets using the findViewById method. For each TextView we provide the relevant text, while for each ImageView we provide a Bitmap that contains the thumbnail image.

A very basic caching mechanism is used at this point in order to avoid re-downloading the same image again and again. Don’t forget that the getView method is going to be called multiple times as the user plays around with the interface, thus we definitely do not wish to perform HTTP requests for the same image. For that reason, a map containing the URL-Bitmap association is used. If the image is not found in the cache, a background task is launched in order to retrieve the image (and store it in the cache for next calls). The background task is named “BitmapDownloaderTask” and extends the AsyncTask class (please check one of our previous tutorial if you wish to find out more on how to use AsyncTasks).

Also note that inside each task, the ImageView instance is referenced via a WeakReference. This is done for performance reasons and more specifically in order to allow the VM’s garbage collector to collect any ImageViews that might belong to a killed activity. In other words, we do not wish an activity to hold strong references of its ImageViews so that those can be easily cleaned up. Check out the official Android developer’s blog post for more information on that.

Regarding the list activity, there are some changes that have to be done there too. The biggest change is that the original ArrayAdapter has been replaced by our custom one. We populate the adapter with the contents of the search results objects and then call the notifyDataSetChanged method on it in order to notify the attached View that the underlying data has been changed and it should refresh itself. This is the code for the new implementation:

package com.javacodegeeks.android.apps.moviesearchapp;

import java.util.ArrayList;

import android.app.ListActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.ListView;
import android.widget.Toast;

import com.javacodegeeks.android.apps.moviesearchapp.model.Movie;
import com.javacodegeeks.android.apps.moviesearchapp.ui.MoviesAdapter;

public class MoviesListActivity extends ListActivity {
    
    private static final String IMDB_BASE_URL = "http://m.imdb.com/title/";
    
    private ArrayList<Movie> moviesList = new ArrayList<Movie>();
    private MoviesAdapter moviesAdapter;
    
    @SuppressWarnings("unchecked")
    @Override
    public void onCreate(Bundle savedInstanceState) {
        
        super.onCreate(savedInstanceState);
        setContentView(R.layout.movies_layout);

        moviesAdapter = new MoviesAdapter(this, R.layout.movie_data_row, moviesList);
        moviesList = (ArrayList<Movie>) getIntent().getSerializableExtra("movies");
        
        setListAdapter(moviesAdapter);
        
        if (moviesList!=null && !moviesList.isEmpty()) {
            
            moviesAdapter.notifyDataSetChanged();
            moviesAdapter.clear();
            for (int i = 0; i < moviesList.size(); i++) {
                moviesAdapter.add(moviesList.get(i));
            }
        }
        
        moviesAdapter.notifyDataSetChanged();
        
    }
    
    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        
        super.onListItemClick(l, v, position, id);
        Movie movie = moviesAdapter.getItem(position);
        
        String imdbId = movie.imdbId;
        if (imdbId==null || imdbId.length()==0) {
            longToast(getString(R.string.no_imdb_id_found));
            return;
        }
        
        String imdbUrl = IMDB_BASE_URL + movie.imdbId;
        
        Intent imdbIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(imdbUrl));                
        startActivity(imdbIntent);
        
    }
    
    public void longToast(CharSequence message) {
        Toast.makeText(this, message, Toast.LENGTH_LONG).show();
    }
    
}

Launch the application and provide a search query. When the “MoviesListActivity” gets triggered, you will first see the list with the movies and the corresponding information. Slowly, the various images will begin to appear as they are being downloaded! Don’t forget that this operations takes places in the background after all. Please note that some movies, especially the older ones, do not have a corresponding thumbnail. Take a look at the following pictures:

That’s it! You can download here the Eclipse project created so far.

Related Articles :

Do you want to know how to develop your skillset to become a Java Rockstar?

Subscribe to our newsletter to start Rocking right now!

To get you started we give you two of our best selling eBooks for FREE!

JPA Mini Book

Learn how to leverage the power of JPA in order to create robust and flexible Java applications. With this Mini Book, you will get introduced to JPA and smoothly transition to more advanced concepts.

JVM Troubleshooting Guide

The Java virtual machine is really the foundation of any Java EE platform. Learn how to master it with this advanced guide!

Given email address is already subscribed, thank you!
Oops. Something went wrong. Please try again later.
Please provide a valid email address.
Thank you, your sign-up request was successful! Please check your e-mail inbox.
Please complete the CAPTCHA.
Please fill in the required fields.

9 Responses to "Android Full App, Part 6: Customized list view for data presentation"

  1. ADNAN HARIS says:

    Nice tutorial. Thanks. It helped me a lot in creating the customized listview and downloading the image in background thread.

  2. babu says:

    03-19 18:17:59.546: E/AndroidRuntime(352): FATAL EXCEPTION: AsyncTask #1

    03-19 18:17:59.546: E/AndroidRuntime(352): java.lang.RuntimeException: An error occured while executing doInBackground()

    03-19 18:17:59.546: E/AndroidRuntime(352): at android.os.AsyncTask$3.done(AsyncTask.java:200)

    03-19 18:17:59.546: E/AndroidRuntime(352): at java.util.concurrent.FutureTask$Sync.innerSetException(FutureTask.java:273)

    03-19 18:17:59.546: E/AndroidRuntime(352): at java.util.concurrent.FutureTask.setException(FutureTask.java:124)

    03-19 18:17:59.546: E/AndroidRuntime(352): at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:307)

    03-19 18:17:59.546: E/AndroidRuntime(352): at java.util.concurrent.FutureTask.run(FutureTask.java:137)

    03-19 18:17:59.546: E/AndroidRuntime(352): at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1068)

    03-19 18:17:59.546: E/AndroidRuntime(352): at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:561)

    03-19 18:17:59.546: E/AndroidRuntime(352): at java.lang.Thread.run(Thread.java:1096)

    03-19 18:17:59.546: E/AndroidRuntime(352): Caused by: java.lang.IllegalArgumentException: Illegal character in path at index 50: http://api.themoviedb.org/2.1/Movie.search/en/xml//kushi

    03-19 18:17:59.546: E/AndroidRuntime(352): at java.net.URI.create(URI.java:970)

    03-19 18:17:59.546: E/AndroidRuntime(352): at org.apache.http.client.methods.HttpGet.(HttpGet.java:75)

    03-19 18:17:59.546: E/AndroidRuntime(352): at com.example.custamizelistviewwithdata.services.HttpRetriever.retrieve(HttpRetriever.java:25)

    03-19 18:17:59.546: E/AndroidRuntime(352): at com.example.custamizelistviewwithdata.services.MovieSeeker.retrieveMoviesList(MovieSeeker.java:40)

    03-19 18:17:59.546: E/AndroidRuntime(352): at com.example.custamizelistviewwithdata.services.MovieSeeker.find(MovieSeeker.java:22)

    03-19 18:17:59.546: E/AndroidRuntime(352): at com.example.custamizelistviewwithdata.PerformMovieSearchTask.doInBackground(PerformMovieSearchTask.java:19)

    03-19 18:17:59.546: E/AndroidRuntime(352): at com.example.custamizelistviewwithdata.PerformMovieSearchTask.doInBackground(PerformMovieSearchTask.java:1)

    03-19 18:17:59.546: E/AndroidRuntime(352): at android.os.AsyncTask$2.call(AsyncTask.java:185)

    03-19 18:17:59.546: E/AndroidRuntime(352): at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:305)

    03-19 18:17:59.546: E/AndroidRuntime(352): … 4 more

    03-19 18:17:59.766: W/IInputConnectionWrapper(352): showStatusIcon on inactive InputConnection

    03-19 18:18:00.995: E/WindowManager(352): Activity com.example.custamizelistviewwithdata.MovieSearchAppActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@43e535a8 that was originally added here

    03-19 18:18:00.995: E/WindowManager(352): android.view.WindowLeaked: Activity com.example.custamizelistviewwithdata.MovieSearchAppActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@43e535a8 that was originally added here

    03-19 18:18:00.995: E/WindowManager(352): at android.view.ViewRoot.(ViewRoot.java:247)

    03-19 18:18:00.995: E/WindowManager(352): at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:148)

    03-19 18:18:00.995: E/WindowManager(352): at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:91)

    03-19 18:18:00.995: E/WindowManager(352): at android.view.Window$LocalWindowManager.addView(Window.java:424)

    03-19 18:18:00.995: E/WindowManager(352): at android.app.Dialog.show(Dialog.java:241)

    03-19 18:18:00.995: E/WindowManager(352): at android.app.ProgressDialog.show(ProgressDialog.java:107)

    03-19 18:18:00.995: E/WindowManager(352): at android.app.ProgressDialog.show(ProgressDialog.java:95)

    03-19 18:18:00.995: E/WindowManager(352): at com.example.custamizelistviewwithdata.MovieSearchAppActivity$2.performSearch(MovieSearchAppActivity.java:78)

    03-19 18:18:00.995: E/WindowManager(352): at com.example.custamizelistviewwithdata.MovieSearchAppActivity$2.onClick(MovieSearchAppActivity.java:71)

    03-19 18:18:00.995: E/WindowManager(352): at android.view.View.performClick(View.java:2408)

    03-19 18:18:00.995: E/WindowManager(352): at android.view.View$PerformClick.run(View.java:8816)

    03-19 18:18:00.995: E/WindowManager(352): at android.os.Handler.handleCallback(Handler.java:587)

    03-19 18:18:00.995: E/WindowManager(352): at android.os.Handler.dispatchMessage(Handler.java:92)

    03-19 18:18:00.995: E/WindowManager(352): at android.os.Looper.loop(Looper.java:123)

    03-19 18:18:00.995: E/WindowManager(352): at android.app.ActivityThread.main(ActivityThread.java:4627)

    03-19 18:18:00.995: E/WindowManager(352): at java.lang.reflect.Method.invokeNative(Native Method)

    03-19 18:18:00.995: E/WindowManager(352): at java.lang.reflect.Method.invoke(Method.java:521)

    03-19 18:18:00.995: E/WindowManager(352): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868)

    03-19 18:18:00.995: E/WindowManager(352): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:626)

    03-19 18:18:00.995: E/WindowManager(352): at dalvik.system.NativeStart.main(Native Method)

  3. David DeMar says:

    For those of you getting this error:

    “java.lang.IllegalStateException: No wrapped connection.”

    It’s caused by multiple threads trying to use the same DefaultHttpClient object. If you take the DefaultHttpClient object declaration and move it inside the retrieve and retrieveStream methods so one is created every time it’s used then that should fix the error.

  4. Dinesh says:

    great tutorial

  5. hulk says:

    hey, I got similar errors as in case of babu……please help me to rectify it

Leave a Reply


× 7 = thirty five



Java Code Geeks and all content copyright © 2010-2014, Exelixis Media Ltd | Terms of Use | Privacy Policy | Contact
All trademarks and registered trademarks appearing on Java Code Geeks are the property of their respective owners.
Java is a trademark or registered trademark of Oracle Corporation in the United States and other countries.
Java Code Geeks is not connected to Oracle Corporation and is not sponsored by Oracle Corporation.
Do you want to know how to develop your skillset and become a ...
Java Rockstar?

Subscribe to our newsletter to start Rocking right now!

To get you started we give you two of our best selling eBooks for FREE!

Get ready to Rock!
You can download the complementary eBooks using the links below:
Close