Creating a RecyclerView with a custom adapter and ViewHolder
A RecyclerView is the standard Android component for displaying repeated content in a scrolling list. It is more flexible and efficient than the older ListView, making it suitable for product catalogues, message screens, news feeds, booking results, and local service directories.
This tutorial builds a small café menu list using Kotlin, XML layouts, a custom adapter, and a ViewHolder. The same structure can support an Australian events app showing listings in Sydney, Melbourne, Brisbane, or regional areas, where users expect smooth scrolling even when mobile coverage or data allowances vary.
Preparing the Android project
Create a new Android Studio project using a basic activity template. Add the RecyclerView library to the module-level Gradle file if it is not already included:
dependencies {
implementation("androidx.recyclerview:recyclerview:1.3.2")
}
The exact version may change as AndroidX libraries are updated, so Android Studio can suggest a current stable release. RecyclerView works with a LayoutManager, an adapter, and a collection of data objects. The layout manager controls positioning, while the adapter connects each object to a row view.
For this example, create a MenuItem data class. It stores the information that will appear in each row:
data class MenuItem(
val name: String,
val description: String,
val price: String
)
Keeping the model separate from the screen makes the project easier to maintain. Later, these values could come from a SQLite database, a REST API, or a JSON response from a café ordering service.
Designing the row layout
Create a file named item_menu.xml in the res/layout directory. This XML file describes one row in the scrolling list:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/textName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/textDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp" />
<TextView
android:id="@+id/textPrice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
</LinearLayout>
A simple vertical layout is enough for a first project, although a ConstraintLayout may be preferable for more complex rows. Use sp for text sizes and dp for spacing so the interface scales across phones and tablets.
Australian users may browse on compact phones while commuting on Sydney trains or waiting for a tram in Melbourne. A clear hierarchy, readable text, and comfortable touch spacing are more valuable than squeezing excessive information into each item.
Building the custom adapter
Create MenuAdapter.kt. The adapter receives a list of MenuItem objects and creates or reuses row views. The nested MenuViewHolder stores references to the row’s views:
class MenuAdapter(
private val items: List<MenuItem>,
private val onItemClick: (MenuItem) -> Unit
) : RecyclerView.Adapter<MenuAdapter.MenuViewHolder>() {
class MenuViewHolder(
itemView: View
) : RecyclerView.ViewHolder(itemView) {
val name: TextView = itemView.findViewById(R.id.textName)
val description: TextView =
itemView.findViewById(R.id.textDescription)
val price: TextView = itemView.findViewById(R.id.textPrice)
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): MenuViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_menu, parent, false)
return MenuViewHolder(view)
}
override fun onBindViewHolder(
holder: MenuViewHolder,
position: Int
) {
val item = items[position]
holder.name.text = item.name
holder.description.text = item.description
holder.price.text = item.price
holder.itemView.setOnClickListener {
onItemClick(item)
}
}
override fun getItemCount(): Int = items.size
}
onCreateViewHolder() inflates a row when RecyclerView needs a new view. onBindViewHolder() places the correct data into that view. getItemCount() tells RecyclerView how many rows are available. The ViewHolder improves performance by retaining references instead of repeatedly searching for each TextView.
Connecting RecyclerView to the activity
Add the RecyclerView to activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/menuRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:clipToPadding="false" />
Then initialise the view in MainActivity.kt:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val menuItems = listOf(
MenuItem("Flat white", "Smooth espresso with steamed milk", "$5.00"),
MenuItem("Avocado toast", "Sourdough with lemon and herbs", "$14.50"),
MenuItem("Lamington", "Classic coconut-covered sponge", "$6.00")
)
val recyclerView = findViewById<RecyclerView>(
R.id.menuRecyclerView
)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = MenuAdapter(menuItems) { selectedItem ->
Toast.makeText(
this,
selectedItem.name,
Toast.LENGTH_SHORT
).show()
}
}
}
The LinearLayoutManager creates a conventional vertical list. A GridLayoutManager can display tiles, while a StaggeredGridLayoutManager supports uneven card heights. For a café finder in Perth or a market directory in Adelaide, a grid may be useful for image-based cards.
Handling clicks and changing data
The adapter above accepts a lambda for click handling. This keeps navigation or feedback in the activity rather than placing screen-specific behaviour inside the adapter. A production app could open a detail screen, add an item to a basket, or show dietary information when a row is tapped.
For data that changes, use a MutableList carefully or adopt ListAdapter with DiffUtil. Calling notifyDataSetChanged() refreshes every visible row, but it may do unnecessary work. More precise methods include notifyItemInserted(), notifyItemRemoved(), and notifyItemChanged().
For example, a menu loaded from a server may change when a café marks an item as unavailable. DiffUtil can calculate the differences between the old and new lists, producing smoother updates and preserving scroll position.
Adding images and accessible content
Many RecyclerView projects display an image beside each row. Add an ImageView to the XML layout, include an image resource or URL in MenuItem, and load remote images with a library such as Coil:
implementation("io.coil-kt:coil:2.6.0")
Inside onBindViewHolder(), an image URL could be loaded with:
holder.image.load(item.imageUrl)
Always provide meaningful content descriptions for informative images. Avoid relying only on colour to communicate availability, dietary status, or selected state. This matters for users using TalkBack and for people checking an app outdoors in bright Australian sunlight.
If the app lists products for a local market, show prices in Australian dollars and format information consistently. A Brisbane food stall, a Hobart bakery, and a Darwin takeaway shop may have different menus, but each benefits from clear labels and predictable touch behaviour.
Testing performance and common errors
Run the app on an emulator and a physical Android device. Test long lists, empty lists, rotated screens, different font sizes, and slow connections. RecyclerView should scroll without noticeable pauses, and each row should display the correct item after scrolling away and back.
A common error is using the wrong layout ID or forgetting to set a layout manager. Another is placing expensive work, such as database queries or image processing, inside onBindViewHolder(). Keep binding lightweight and load data before it reaches the adapter, or use asynchronous tools such as ViewModel and Room.
Check that row views do not use an unnecessarily heavy hierarchy. Recycled views are efficient, but very complex cards can still consume memory. Test on lower-cost devices commonly used across Australia, not just a recent flagship handset, and consider offline or cached content for users travelling through regional areas where network access can be inconsistent.