The list view we are going to build contains list of movies information where each list row defines single movie. The row layout is customized that contains movie poster as thumbnail image on the left and the movie title, rating, genre and release date on the right. Below is the final output.

Download Code

Video Demo

Final Output

Android Custom ListView with Image and Text using Volley1. Demo JSON

JSON URL: http://api.codedecode.in/json/movies/movie.json

[
        {
        "title": "Dawn of the Planet of the Apes",
        "image": "http://api.codedecode.in/json/movies/1.jpg",
        "rating": 8.3,
        "releaseYear": 2014,
        "genre": ["Action", "Drama", "Sci-Fi"]
        },
        ....
        ....
        ]

2. Creating New Project

1. In Android Studio, go to File ⇒ New ⇒ New Project and fill all the details required to create a new project. When it prompts to select a default activity, select Blank Activity and proceed.

2. Add below dependency in Build.gradle file.

dependencies {
    implementation 'com.android.volley:volley:1.1.1'
}

3. I am creating required packages first just to keep the project organised. This step is optional but it is recommended. Create four packages named adapterappmodel and util.

4. Open colors.xml under res ⇒ values and add below colors. If you don’t see colors.xml file, create a new file and add these color values.

colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <color name="genre">#666666</color>
    <color name="year">#888888</color>
    <color name="list_divider">#d9d9d9</color>
    <color name="list_row_start_color">#ffffff</color>
    <color name="list_row_end_color">#ffffff</color>
    <color name="list_row_hover_start_color">#ebeef0</color>
    <color name="list_row_hover_end_color">#ebeef0</color>

</resources>

5. Also add below dimensions in dimens.xml file located under res ⇒ values.

dimens.xml

<resources>
        
    <dimen name="title">17dp</dimen>
    <dimen name="rating">15dip</dimen>
    <dimen name="genre">13dip</dimen>
    <dimen name="year">12dip</dimen>

</resources>

6. Before start writing java code, I would like to complete the UI part first. Create list_row_bg.xmllist_row_bg_hover.xml and list_row_selector.xml with below respective codes under res ⇒ drawable folder. If you don’t see drawable folder under res, create a new folder and name it as drawable.

list_row_bg.xml – Default style for list view row.

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
  <gradient
      android:startColor="@color/list_row_start_color"
      android:endColor="@color/list_row_end_color"
      android:angle="270" />
</shape>

list_row_bg_hover.xml – Style for list view row when row is selected

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >

    <gradient
        android:angle="270"
        android:endColor="@color/list_row_hover_end_color"
        android:startColor="@color/list_row_hover_start_color" />

</shape>

list_row_selector.xml – Selector file to toggle the row states.

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

    <item
        android:drawable="@drawable/list_row_bg"
        android:state_pressed="false"
        android:state_selected="false"/>
    <item
        android:drawable="@drawable/list_row_bg_hover"
        android:state_pressed="true"/>
    <item
        android:drawable="@drawable/list_row_bg_hover"
        android:state_pressed="false"
        android:state_selected="true"/>

</selector>

7. Now open your layout file of activity_main.xml and add a ListView element.

activity_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"
    tools:context=".MainActivity" >

    <ListView
        android:id="@+id/list"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:divider="@color/list_divider"
        android:dividerHeight="1dp"
        android:listSelector="@drawable/list_row_selector" />

</RelativeLayout>

8. We need to create another layout file for list view row. This is the main design component in this project as we define actual custom list design here. I am naming this file as list_row.xml.

list_row.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/list_row_selector"
    android:padding="8dp" >

    <!-- Thumbnail Image -->
    <com.android.volley.toolbox.NetworkImageView
        android:id="@+id/thumbnail"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_alignParentLeft="true"
        android:layout_marginRight="8dp"
        android:layout_alignParentStart="true"
        android:layout_marginEnd="8dp" />

    <!-- Movie Title -->
    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignTop="@+id/thumbnail"
        android:layout_toRightOf="@+id/thumbnail"
        android:textSize="@dimen/title"
        android:textStyle="bold"
        android:layout_toEndOf="@+id/thumbnail" />

    <!-- Rating -->
    <TextView
        android:id="@+id/rating"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/title"
        android:layout_marginTop="1dip"
        android:layout_toRightOf="@+id/thumbnail"
        android:textSize="@dimen/rating"
        android:layout_toEndOf="@+id/thumbnail" />
    
    <!-- Genre -->
    <TextView
        android:id="@+id/genre"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/rating"
        android:layout_marginTop="5dp"
        android:layout_toRightOf="@+id/thumbnail"
        android:textColor="@color/genre"
        android:textSize="@dimen/genre"
        android:layout_toEndOf="@+id/thumbnail" />

    <!-- Release Year -->
    <TextView
        android:id="@+id/releaseYear"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:textColor="@color/year"
        android:textSize="@dimen/year"
        android:layout_alignParentEnd="true" />

</RelativeLayout>

Here we completed the design part. Let’s move to java part.

9. Create LruBitmapCache.java under util package. This class takes care of caching images on disk.

package com.codedecode.customlistviewvolley.util;

import com.android.volley.toolbox.ImageLoader.ImageCache;
import android.graphics.Bitmap;
import android.util.LruCache;

public class LruBitmapCache extends LruCache<String, Bitmap> implements
      ImageCache {
   private static int getDefaultLruCacheSize() {
      final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

      return maxMemory / 8;
   }

   public LruBitmapCache() {
      this(getDefaultLruCacheSize());
   }

   private LruBitmapCache(int sizeInKiloBytes) {
      super(sizeInKiloBytes);
   }

   @Override
   protected int sizeOf(String key, Bitmap value) {
      return value.getRowBytes() * value.getHeight() / 1024;
   }

   @Override
   public Bitmap getBitmap(String url) {
      return get(url);
   }

   @Override
   public void putBitmap(String url, Bitmap bitmap) {
      put(url, bitmap);
   }
}

10. Create AppController.java under app package. This class is a singleton class which initializes core objects of volley library.

AppController.java

package com.codedecode.customlistviewvolley.app;

import android.app.Application;
import android.text.TextUtils;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.Volley;
import com.codedecode.customlistviewvolley.util.LruBitmapCache;

public class AppController extends Application {

   public static final String TAG = AppController.class.getSimpleName();

   private RequestQueue mRequestQueue;
   private ImageLoader mImageLoader;

   private static AppController mInstance;

   @Override
   public void onCreate() {
      super.onCreate();
      mInstance = this;
   }

   public static synchronized AppController getInstance() {
      return mInstance;
   }

   public RequestQueue getRequestQueue() {
      if (mRequestQueue == null) {
         mRequestQueue = Volley.newRequestQueue(getApplicationContext());
      }

      return mRequestQueue;
   }

   public ImageLoader getImageLoader() {
      getRequestQueue();
      if (mImageLoader == null) {
         mImageLoader = new ImageLoader(this.mRequestQueue,
               new LruBitmapCache());
      }
      return this.mImageLoader;
   }

   public <T> void addToRequestQueue(Request<T> req, String tag) {
      // set the default tag if tag is empty
      req.setTag(TextUtils.isEmpty(tag) ? TAG : tag);
      getRequestQueue().add(req);
   }

   public <T> void addToRequestQueue(Request<T> req) {
      req.setTag(TAG);
      getRequestQueue().add(req);
   }

   public void cancelPendingRequests(Object tag) {
      if (mRequestQueue != null) {
         mRequestQueue.cancelAll(tag);
      }
   }
}

11. Create  res ⇒ xml Folder and Add Network Security Config in res ⇒ xml ⇒ network_security_config.xml which we will add in AndroidManifest.xml file.

network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">api.codedecode.in</domain>
    </domain-config>
</network-security-config>

12. Now add the AppController.java class in AndroidManifest.xml to your <application> tag using name property to execute this class on application start. Also add INTERNET permission as we are going to make network calls And Netwok and add network_security_config.

<uses-permission android:name="android.permission.INTERNET" />
<application
    android:networkSecurityConfig="@xml/network_security_config"
    android:name="com.codedecode.customlistviewvolley.app.AppController" .../>

AndroidManifest.xml 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.codedecode.customlistviewvolley">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:networkSecurityConfig="@xml/network_security_config"
        android:name="com.codedecode.customlistviewvolley.app.AppController"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

13. Now create Movie.java under model package. This model class will be used to provide movie objects data to list view after parsing the json.

package com.codedecode.customlistviewvolley.model;

import java.util.ArrayList;

public class Movie {
   private String title, thumbnailUrl;
   private int year;
   private double rating;
   private ArrayList<String> genre;

   public Movie() {
   }

   public Movie(String name, String thumbnailUrl, int year, double rating,
         ArrayList<String> genre) {
      this.title = name;
      this.thumbnailUrl = thumbnailUrl;
      this.year = year;
      this.rating = rating;
      this.genre = genre;
   }

   public String getTitle() {
      return title;
   }

   public void setTitle(String name) {
      this.title = name;
   }

   public String getThumbnailUrl() {
      return thumbnailUrl;
   }

   public void setThumbnailUrl(String thumbnailUrl) {
      this.thumbnailUrl = thumbnailUrl;
   }

   public int getYear() {
      return year;
   }

   public void setYear(int year) {
      this.year = year;
   }

   public double getRating() {
      return rating;
   }

   public void setRating(double rating) {
      this.rating = rating;
   }

   public ArrayList<String> getGenre() {
      return genre;
   }

   public void setGenre(ArrayList<String> genre) {
      this.genre = genre;
   }

}

13. Create a class named CustomListAdapter.java with below code. This is a custom list adapter class which provides data to list view. In other words it renders the layout_row.xml in list by pre-filling appropriate information.

CustomListAdapter.java

package com.codedecode.customlistviewvolley.adater;

import android.app.Activity;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import com.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.NetworkImageView;
import com.codedecode.customlistviewvolley.R;
import com.codedecode.customlistviewvolley.app.AppController;
import com.codedecode.customlistviewvolley.model.Movie;
import java.util.List;

public class CustomListAdapter extends BaseAdapter {
   private Activity activity;
   private LayoutInflater inflater;
   private List<Movie> movieItems;
   private ImageLoader imageLoader = AppController.getInstance().getImageLoader();

   public CustomListAdapter(Activity activity, List<Movie> movieItems) {
      this.activity = activity;
      this.movieItems = movieItems;
   }

   @Override
   public int getCount() {
      return movieItems.size();
   }

   @Override
   public Object getItem(int location) {
      return movieItems.get(location);
   }

   @Override
   public long getItemId(int position) {
      return position;
   }

   @Override
   public View getView(int position, View convertView, ViewGroup parent) {

      if (inflater == null)
         inflater = (LayoutInflater) activity
               .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
      if (convertView == null)
         convertView = inflater.inflate(R.layout.list_row, null);

      if (imageLoader == null)
         imageLoader = AppController.getInstance().getImageLoader();
      NetworkImageView thumbNail = (NetworkImageView) convertView
            .findViewById(R.id.thumbnail);
      TextView title = convertView.findViewById(R.id.title);
      TextView rating = convertView.findViewById(R.id.rating);
      TextView genre = convertView.findViewById(R.id.genre);
      TextView year = convertView.findViewById(R.id.releaseYear);

      // getting movie data for the row
      Movie m = movieItems.get(position);

      // thumbnail image
      thumbNail.setImageUrl(m.getThumbnailUrl(), imageLoader);
      
      // title
      title.setText(m.getTitle());
      
      // rating
      rating.setText("Rating: " + String.valueOf(m.getRating()));
      
      // genre
      String genreStr = "";
      for (String str : m.getGenre()) {
         genreStr += str + ", ";
      }
      genreStr = genreStr.length() > 0 ? genreStr.substring(0,
            genreStr.length() - 2) : genreStr;
      genre.setText(genreStr);
      
      // release year
      year.setText(String.valueOf(m.getYear()));

      return convertView;
   }

}

14. Now open your MainActivity.java and do the below changes. Here I created volley’s JsonArrayRequest to get the json from url. Upon parsing the json, I stored all the json data into an ArrayList as Movie objects. Finally I called notifyDataSetChanged() on CustomListAdapter instance to render the list view with updated information.

package com.codedecode.customlistviewvolley;

import java.util.ArrayList;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.ProgressDialog;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.widget.ListView;
import androidx.appcompat.app.AppCompatActivity;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.VolleyLog;
import com.android.volley.toolbox.JsonArrayRequest;
import com.codedecode.customlistviewvolley.adater.CustomListAdapter;
import com.codedecode.customlistviewvolley.app.AppController;
import com.codedecode.customlistviewvolley.model.Movie;

public class MainActivity extends AppCompatActivity {
    // Log tag
    private static final String TAG = MainActivity.class.getSimpleName();

    // Movies json url
    private static final String url = "http://api.codedecode.in/json/movies/movie.json";
    private ProgressDialog pDialog;
    private List<Movie> movieList = new ArrayList<Movie>();
    private CustomListAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ListView listView = findViewById(R.id.list);
        adapter = new CustomListAdapter(this, movieList);
        listView.setAdapter(adapter);

        pDialog = new ProgressDialog(this);
        // Showing progress dialog before making http request
        pDialog.setMessage("Loading...");
        pDialog.show();

        // changing action bar color
        getSupportActionBar().setBackgroundDrawable(
                new ColorDrawable(Color.parseColor("#1b1b1b")));

        // Creating volley request obj
        JsonArrayRequest movieReq = new JsonArrayRequest(url,
                new Response.Listener<JSONArray>() {
                    @Override
                    public void onResponse(JSONArray response) {
                        Log.d(TAG, response.toString());
                        hidePDialog();

                        // Parsing json
                        for (int i = 0; i < response.length(); i++) {
                            try {

                                JSONObject obj = response.getJSONObject(i);
                                Movie movie = new Movie();
                                movie.setTitle(obj.getString("title"));
                                movie.setThumbnailUrl(obj.getString("image"));
                                movie.setRating(((Number) obj.get("rating"))
                                        .doubleValue());
                                movie.setYear(obj.getInt("releaseYear"));

                                // Genre is json array
                                JSONArray genreArry = obj.getJSONArray("genre");
                                ArrayList<String> genre = new ArrayList<String>();
                                for (int j = 0; j < genreArry.length(); j++) {
                                    genre.add((String) genreArry.get(j));
                                }
                                movie.setGenre(genre);

                                // adding movie to movies array
                                movieList.add(movie);

                            } catch (JSONException e) {
                                e.printStackTrace();
                            }
                        }
                        // notifying list adapter about data changes
                        // so that it renders the list view with updated data
                        adapter.notifyDataSetChanged();
                    }
                }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                VolleyLog.d(TAG, "Error: " + error.getMessage());
                hidePDialog();
            }
        });

        // Adding request to request queue
        AppController.getInstance().addToRequestQueue(movieReq);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        hidePDialog();
    }

    private void hidePDialog() {
        if (pDialog != null) {
            pDialog.dismiss();
            pDialog = null;
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu, menu);
        return true;
    }
}

Happy Coding 🙂