Visit Sponsor

Written by 4:20 pm Android

Android Asynchronous Image Loader in ListView

Loading images in a ListView without blocking the UI thread is essential for smooth performance. Synchronous loading causes freezes, jank, and poor user experience. Modern Android development mandates asynchronous loading coupled with efficient caching.

This updated guide on javatechig.com explains how to implement asynchronous image loading in a ListView using modern tools, lifecycle-safe practices, and efficient memory handling with both Kotlin and Java.

Why Asynchronous Image Loading Matters

When you load images on the main thread:

  • The UI blocks
  • Scrolling becomes janky
  • Memory spikes
  • Users abandon your app

To fix this, images must load off the main thread, updated back onto the UI when ready.

Modern Approach (Recommended): Use Image Loading Libraries

Instead of reinventing the wheel with raw threads or custom AsyncTasks (now discouraged), use well-maintained, optimized libraries:

Popular Libraries

  • Glide — Efficient image loading, caching, and lifecycle integration
  • Coil — Kotlin-first, coroutine based
  • Picasso — Simple, easy to use

These handle threading, caching (memory & disk), and View recycling automatically.

Example: Using Glide in ListView Adapter

Step 1 — Add Dependency

In module build.gradle:

implementation 'com.github.bumptech.glide:glide:4.15.1'
kapt 'com.github.bumptech.glide:compiler:4.15.1'

Adapter Implementation (Kotlin)

class ImageListAdapter(private val context: Context, private val items: List<String>)
    : BaseAdapter() {

    override fun getCount() = items.size
    override fun getItem(position: Int) = items[position]
    override fun getItemId(position: Int) = position.toLong()

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val view = convertView ?: LayoutInflater.from(context)
            .inflate(R.layout.list_item, parent, false)

        val imageView = view.findViewById<ImageView>(R.id.imageView)

        Glide.with(context)
            .load(items[position])
            .placeholder(R.drawable.placeholder)
            .error(R.drawable.error_image)
            .into(imageView)

        return view
    }
}

Kotlin libraries like Coil work similarly:

imageView.load(items[position]) {
    placeholder(R.drawable.placeholder)
    error(R.drawable.error_image)
}

Java ListView Adapter (Glide)

public class ImageListAdapter extends BaseAdapter {
    private Context context;
    private List<String> images;

    public ImageListAdapter(Context context, List<String> images) {
        this.context = context;
        this.images = images;
    }

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

    @Override
    public Object getItem(int position) { return images.get(position); }

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            convertView = LayoutInflater.from(context)
                .inflate(R.layout.list_item, parent, false);
        }

        ImageView imageView = convertView.findViewById(R.id.imageView);

        Glide.with(context)
            .load(images.get(position))
            .placeholder(R.drawable.placeholder)
            .error(R.drawable.error_image)
            .into(imageView);

        return convertView;
    }
}

Why ViewHolder & Recycling Matters

ListView reuses item views. Without caching and proper recycling:

  • Images may flicker
  • Wrong images can appear
  • Memory spikes

Using libraries like Glide or Coil ensures images cancel old requests when views are reused.

Using ViewHolder Explicitly (Legacy Example)

class ViewHolder(view: View) {
    val imageView: ImageView = view.findViewById(R.id.imageView)
}

override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    val holder: ViewHolder
    val view: View

    if (convertView == null) {
        view = LayoutInflater.from(context).inflate(R.layout.list_item, parent, false)
        holder = ViewHolder(view)
        view.tag = holder
    } else {
        view = convertView
        holder = convertView.tag as ViewHolder
    }

    Glide.with(context)
        .load(items[position])
        .into(holder.imageView)

    return view
}

Even with ViewHolder, libraries handle background loading and caching for you.

Caching Benefits

Image loading libraries include:

Memory Cache

Quick retrieval during scrolling

Disk Cache

Persistent store for repeated requests

These significantly improve performance and reduce network overhead.

Common Issues & Fixes

ListView Scroll Lag

Cause: Heavy image decoding on main thread
Fix: Always use libraries with background decoding

Wrong Image Appears

Cause: View reuse before load finishes
Fix: Libraries cancel previous requests automatically

OOM (OutOfMemory)

Cause: Large bitmaps kept in memory
Fix: Use placeholder, resizing, and proper caching

Best Practices (2026 Updated)

  • Prefer RecyclerView over ListView for modern apps
  • Use Glide / Coil / Picasso for async image loading
  • Resize images to appropriate sizes before loading
  • Use disk and memory caching effectively
  • Avoid AsyncTask — prefer library managed threads
  • Respect lifecycle with Glide/Coil integrations

When ListView May Still Be Used

While RecyclerView is recommended for most modern apps due to flexibility and performance, legacy codebases may still use ListView. In such cases, using Glide or Coil ensures acceptable performance.

Visited 5 times, 1 visit(s) today
Close