Creating a RecyclerView With Click Listeners in Android

A RecyclerView is the standard Android component for displaying a scrolling collection of items. It is more flexible and efficient than the older ListView, making it suitable for product catalogues, messages, travel results, news feeds and settings screens.

A useful RecyclerView implementation has three parts: a layout for each row, an adapter that binds data to those rows, and a click-handling strategy. Keeping these responsibilities separate makes the code easier to test and adapt when the app grows.

This approach works well for Android projects built in Kotlin and Android Studio. It also suits Australian audiences, whether an app serves commuters in Sydney, shoppers in Melbourne or users in regional areas where efficient scrolling and modest data usage matter.

The example below creates a list of cafés. Each row displays a café name and suburb, then sends the selected item back to the screen when tapped. The same pattern can be adapted for products, bookings, contacts or search results.

Set Up The RecyclerView Layout

Start with a screen layout containing a RecyclerView. Give it an ID so the activity or fragment can find it, and use a LinearLayoutManager for a conventional vertical list.

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/cafeRecyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    android:clipToPadding="false" />

Create a separate XML file called item_cafe.xml for one row. A MaterialCardView gives each item a clear touch target, while a vertical layout holds the café name and suburb.

<com.google.android.material.card.MaterialCardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="10dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

        <TextView
            android:id="@+id/cafeName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/cafeSuburb"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="4dp" />
    </LinearLayout>
</com.google.android.material.card.MaterialCardView>

The row should have enough height and padding for comfortable tapping. This matters for people using a phone while travelling on a train between Parramatta and central Sydney, or while carrying shopping bags through a Melbourne market.

Build The Adapter And ViewHolder

Define a model representing one item in the collection. A Kotlin data class keeps the example concise and makes it easy to add fields later, such as an image URL, rating or opening status.

data class Cafe(
    val name: String,
    val suburb: String
)

The adapter inflates the row layout and binds each Cafe object to its views. The click listener is passed into the adapter rather than placing navigation logic directly inside the adapter. This keeps the adapter focused on displaying content.

class CafeAdapter(
    private val cafes: List<Cafe>,
    private val onCafeClick: (Cafe) -> Unit
) : RecyclerView.Adapter<CafeAdapter.CafeViewHolder>() {

    class CafeViewHolder(
        itemView: View
    ) : RecyclerView.ViewHolder(itemView) {
        val name: TextView = itemView.findViewById(R.id.cafeName)
        val suburb: TextView = itemView.findViewById(R.id.cafeSuburb)
    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): CafeViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_cafe, parent, false)
        return CafeViewHolder(view)
    }

    override fun onBindViewHolder(
        holder: CafeViewHolder,
        position: Int
    ) {
        val cafe = cafes[position]
        holder.name.text = cafe.name
        holder.suburb.text = cafe.suburb

        holder.itemView.setOnClickListener {
            onCafeClick(cafe)
        }
    }

    override fun getItemCount(): Int = cafes.size
}

Passing the actual Cafe object to the callback is safer than passing only a position. Positions can change when data is filtered, sorted or refreshed, while the object represents the item the user selected at bind time.

Connect The Listener In An Activity Or Fragment

In an activity, initialise the RecyclerView, create the data source and assign the adapter. The callback can open a detail screen, display a message or trigger another application action.

class CafeListActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_cafe_list)

        val cafes = listOf(
            Cafe("Harbour Brew", "Circular Quay"),
            Cafe("Northside Coffee", "Brunswick"),
            Cafe("Riverbank Roasters", "South Brisbane")
        )

        val recyclerView = findViewById<RecyclerView>(
            R.id.cafeRecyclerView
        )

        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = CafeAdapter(cafes) { selectedCafe ->
            Toast.makeText(
                this,
                "Selected ${selectedCafe.name}",
                Toast.LENGTH_SHORT
            ).show()
        }
    }
}

For a fragment, use view.findViewById after inflating the fragment view, or use View Binding. A shared listener can call Navigation Component actions, such as opening CafeDetailFragment with an ID stored in a Bundle.

Avoid capturing a stale adapter position in a long-running callback. If your app uses ListAdapter, DiffUtil or asynchronous updates, use bindingAdapterPosition when a current position is required, and check that it is not RecyclerView.NO_POSITION.

Choose A Listener Pattern

There are several valid ways to handle item interaction. A lambda is compact and ideal for a small screen with one clear action. An interface can be useful when several event types need to be exposed, such as item selection, favourite toggling and an overflow menu.

Pattern Suitable For Strength Watch For
Lambda callback One primary row action Concise and readable Can become crowded with many events
Listener interface Several adapter events Explicit and scalable Requires more boilerplate
ViewModel event Complex screens and shared state Works well with lifecycle-aware UI Needs careful event handling
XML click handler Very simple static actions Quick to prototype Less flexible for dynamic lists

A row click should represent the main action, while smaller controls should have independent listeners. For example, a shopping app might let a tap open product details and provide a separate heart button for saving an item. Give each control a meaningful content description and avoid making the entire row clickable when it contains competing actions.

Test Touch Behaviour And List Updates

RecyclerView interactions should be tested with realistic content. Include short and long café names, empty results, hundreds of items and records with missing optional values. Test on different screen sizes and orientations, including a smaller handset commonly used for quick payments or public transport checks.

A reliable checklist includes:

  • Tap the text and empty padding within the row.
  • Confirm the selected object matches the visible item.
  • Scroll quickly before tapping an item.
  • Rotate the device and check restored state.

List updates deserve special attention when content comes from a server. Use ListAdapter with DiffUtil for changing collections instead of repeatedly calling notifyDataSetChanged(). This produces smoother animations and reduces unnecessary view work, which helps users on mobile connections outside major Australian cities.

Also check privacy and consumer expectations before releasing an app. If a click opens a venue profile that collects location or contact information, explain that use clearly and handle personal data in line with the Australian Privacy Act. If the app lists prices, delivery terms or subscriptions, present them accurately under Australian Consumer Law.

Improve The RecyclerView For Production

For production apps, replace the plain list with a repository or ViewModel that owns the data. The activity or fragment should observe state and render it, while the adapter should bind rows and report interactions. This separation prevents click listeners from becoming tangled with network requests and navigation code.

Stable IDs, payload updates and image loading libraries can improve performance when the list becomes large. Use Glide or Coil for remote images, provide placeholders, and avoid downloading a full-resolution image for a small thumbnail. A local cache also helps users browsing while commuting through areas with inconsistent coverage.

Empty and error states are part of the interaction design. Show a useful message when no cafés match a suburb, and provide a retry action when a request fails. If the list supports accessibility services, ensure labels identify the item and action clearly, and keep touch targets large enough for confident use.

Add this pattern to a small Android project first, then extend it with detail navigation, filtering and asynchronous data. A clean adapter, a clear callback and lifecycle-aware state management will give your RecyclerView a dependable foundation for real Australian users and changing production data.