Creating a Custom ListView Adapter with View Recycling
A ListView displays rows by asking an adapter for the view required at each position. A custom adapter gives you control over how each row looks, how data is formatted, and how user interactions are handled. View recycling then prevents Android from creating a brand-new row for every item.
This pattern remains useful when maintaining older Android applications, learning adapter fundamentals, or working through a small project where RecyclerView would add unnecessary complexity. The same principles also help explain how modern list components manage performance.
| Approach | Best Use | Main Trade-Off |
|---|---|---|
Basic ArrayAdapter |
Simple text-only rows | Limited layout control |
Custom BaseAdapter |
Custom row designs and actions | More code to maintain |
| Custom adapter with ViewHolder | Efficient repeated rows | Requires careful binding |
RecyclerView.Adapter |
Large, modern applications | More setup and concepts |
For Australian developers, a list might show café orders in Melbourne, bus routes around Brisbane, or event venues near Sydney’s Circular Quay. These examples can contain long names, different price formats, and variable text lengths, making a carefully designed row more important than a quick placeholder layout.
The examples below use Java and a simple model class. You can adapt them for a Kotlin project, a SQLite cursor, or data loaded from a web service. If you are organising snippets in a programmable editor alongside Android Studio, a handy Emacs resource can also fit into that broader development workflow.
Preparing The Row Data
Start with a model object that represents one item in the list. Keeping data in a separate class makes the adapter easier to read and allows the same objects to be used by a database layer or a network response.
public class Cafe {
private final String name;
private final String suburb;
private final double rating;
public Cafe(String name, String suburb, double rating) {
this.name = name;
this.suburb = suburb;
this.rating = rating;
}
public String getName() { return name; }
public String getSuburb() { return suburb; }
public double getRating() { return rating; }
}
Create a layout file called row_cafe.xml. A horizontal LinearLayout containing two TextView controls is enough for this example. Give the controls stable IDs such as textName and textDetails, and keep the row height comfortable for touch input. This matters when the list contains suburbs such as Parramatta or South Yarra, where labels may need more room than a short test value.
Building The Custom Adapter
A custom adapter can extend BaseAdapter. It receives the application context and the list of model objects, then reports how many rows are available and returns the appropriate item for each position.
public class CafeAdapter extends BaseAdapter {
private final LayoutInflater inflater;
private final List<Cafe> cafes;
public CafeAdapter(Context context, List<Cafe> cafes) {
this.inflater = LayoutInflater.from(context);
this.cafes = cafes;
}
@Override
public int getCount() {
return cafes.size();
}
@Override
public Object getItem(int position) {
return cafes.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
return null;
}
}
getCount() tells the ListView how many items exist, while getItem() returns the object at a position. getItemId() can return a database ID when one exists; using the position is acceptable for a static demonstration. The important method is getView(), where the row is either inflated for the first time or reused from an earlier screen position.
Adding View Recycling With A ViewHolder
Without recycling, repeated calls to inflate() can create many temporary view objects as the user scrolls. The convertView parameter is Android’s opportunity to supply a row that has moved off-screen. The adapter should reuse it whenever it is available.
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = inflater.inflate(R.layout.row_cafe, parent, false);
holder = new ViewHolder();
holder.name = convertView.findViewById(R.id.textName);
holder.details = convertView.findViewById(R.id.textDetails);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
Cafe cafe = cafes.get(position);
holder.name.setText(cafe.getName());
holder.details.setText(
cafe.getSuburb() + " · " + cafe.getRating() + " stars"
);
return convertView;
}
static class ViewHolder {
TextView name;
TextView details;
}
The ViewHolder stores references to the child views, so findViewById() is performed only when the row is first created. Every visible row must still be fully bound each time. If a row previously displayed an image, badge, or hidden label, reset that state when the next item does not need it; recycled views retain their old values.
Connecting The Adapter To A ListView
In an activity or fragment, obtain the ListView, prepare the data, and assign the adapter. For a fragment, call findViewById() on the inflated root view rather than directly on the activity.
List<Cafe> cafes = new ArrayList<>();
cafes.add(new Cafe("Harbour Brew", "Sydney", 4.6));
cafes.add(new Cafe("Laneway Beans", "Melbourne", 4.8));
cafes.add(new Cafe("Riverbend Coffee", "Brisbane", 4.4));
ListView cafeList = findViewById(R.id.cafeList);
CafeAdapter adapter = new CafeAdapter(this, cafes);
cafeList.setAdapter(adapter);
An item click listener can retrieve the selected model object rather than relying on the displayed text.
cafeList.setOnItemClickListener((parent, view, position, id) -> {
Cafe selected = cafes.get(position);
Toast.makeText(
this,
"Selected " + selected.getName(),
Toast.LENGTH_SHORT
).show();
});
If the list is refreshed after adding a café or receiving SQLite results, call adapter.notifyDataSetChanged(). For larger or frequently changing datasets, update the underlying collection carefully and consider migrating to RecyclerView, which offers stronger support for modern data updates.
Checking Performance And Row Behaviour
A recycled adapter should scroll smoothly on an ordinary Android handset, including budget devices commonly used with prepaid plans across Australia. Test rows with short and long names, empty values, unusually high ratings, and different screen sizes. Check both portrait and landscape layouts if the activity supports rotation.
Useful checks before treating the adapter as finished include:
- Confirm that
convertViewis reused after the first visible screen. - Make sure every
TextViewis assigned on every binding pass. - Verify that taps select the correct position after scrolling.
- Check that an empty list displays a clear empty-state message.
You can also inspect the result using Android Studio’s Layout Inspector and profiler. Watch for unnecessary object creation inside getView(), especially repeated date formatting, bitmap decoding, or database calls. Expensive work belongs outside the adapter or in a cached data layer.
For practical projects, keep the adapter focused on displaying data. Let a repository load SQLite records, let a view model manage screen state, and let the activity or fragment respond to clicks. This separation keeps a tutorial-sized ListView understandable while making it easier to extend into a production application used by people from Perth to Hobart.