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.


