Android SharedPreferences Tutorial and Example

Implementing Clickable RecyclerView Items in Android

A RecyclerView displays a scrolling collection of views, but each row becomes useful when it responds to a tap. A click listener can open a detail screen, select a record, start an edit flow, or remove an item from a shopping list. The cleanest implementation keeps the click event connected to the item data rather than hiding navigation logic inside the adapter.

This tutorial uses Kotlin and a simple list of products. The same pattern works for contacts, messages, SQLite records, or saved preferences. It is suitable for an Android app used on a small phone in Brisbane, a tablet in a Melbourne café, or a device tested during a Sydney commute.

Before adding the listener, decide what the callback should receive. Passing the complete model object is usually more useful than passing only a position, because the position may change after filtering, insertion, or deletion.

Approach Best use Main benefit Watch point
Listener in the adapter Small demonstrations Quick to implement Can mix UI and navigation logic
Callback interface Reusable screens Keeps click handling outside the adapter Requires a little more setup
Lambda function Modern Kotlin projects Concise and readable Needs a clear adapter API
Android ListAdapter Frequently changing lists Handles updates efficiently Requires DiffUtil

Create The Item Model And Layout

Start with a data class representing one row. In a real application, this could contain an SQLite ID, a product name, a price in Australian dollars, and a category. The stable ID gives the click handler something reliable to use even when the visible list changes.

data class Product(
    val id: Long,
    val name: String,
    val price: Double
)

Create item_product.xml with a root layout and two text views. A MaterialCardView works well for a modern interface, although a simple ConstraintLayout is enough for a tutorial. Add android:clickable="true" and android:focusable="true" to the root if you want clearer keyboard and accessibility behaviour.

The row should provide a visible pressed state through a selectable background or card ripple. This matters on phones used outdoors in places such as Perth, where a user may tap quickly while moving between shops, and it gives useful feedback before the next screen appears.

Define A Callback In The Adapter

The adapter can accept a function that receives the selected product. This keeps the adapter responsible for binding data and reporting taps, while the activity or fragment decides what a tap means.

class ProductAdapter(
    private val products: List<Product>,
    private val onProductClick: (Product) -> Unit
) : RecyclerView.Adapter<ProductAdapter.ProductViewHolder>() {

    class ProductViewHolder(
        private val binding: ItemProductBinding
    ) : RecyclerView.ViewHolder(binding.root) {

        fun bind(product: Product, onProductClick: (Product) -> Unit) {
            binding.nameTextView.text = product.name
            binding.priceTextView.text =
                NumberFormat.getCurrencyInstance(Locale("en", "AU"))
                    .format(product.price)

            binding.root.setOnClickListener {
                onProductClick(product)
            }
        }
    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): ProductViewHolder {
        val binding = ItemProductBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        return ProductViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
        holder.bind(products[position], onProductClick)
    }

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

The important line is binding.root.setOnClickListener. It attaches the click listener to the whole row, making the complete item tappable. The callback receives product, not position, so the receiving screen can use the record’s ID or other properties safely.

Avoid capturing a stale adapter position inside a long-running operation. If the callback needs the current position, use holder.bindingAdapterPosition inside the click event and check that it is not RecyclerView.NO_POSITION. For most navigation tasks, passing the model or its stable ID is simpler.

Connect The Listener In A Fragment

In a fragment, initialise the RecyclerView, create the adapter, and provide a lambda for the click action. The lambda can navigate to a detail fragment using the product ID. This keeps navigation in the screen controller instead of coupling the adapter to a particular activity.

private fun setupProducts(products: List<Product>) {
    val adapter = ProductAdapter(products) { selectedProduct ->
        val action =
            ProductListFragmentDirections
                .actionProductListToProductDetails(selectedProduct.id)

        findNavController().navigate(action)
    }

    binding.productRecyclerView.layoutManager =
        LinearLayoutManager(requireContext())
    binding.productRecyclerView.adapter = adapter
}

If the destination needs several properties, pass a parcelable model or load the record by ID from a repository. For small demonstrations, a Product object is convenient. For larger applications, an ID avoids passing stale data and lets the detail screen load the latest state.

You may also want to remember which item was last opened. SharedPreferences can store a small value such as an ID, while a serialised model can be handled with persisting complex objects. Use this selectively: a database or DataStore is generally better for larger or frequently changing data.

Handle Buttons Inside Each Row

A row may contain a separate favourite button, menu icon, or add-to-cart control. In that case, attach independent callbacks rather than assuming every tap means “open details”.

class ProductViewHolder(
    private val binding: ItemProductBinding
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(
        product: Product,
        onProductClick: (Product) -> Unit,
        onFavouriteClick: (Product) -> Unit
    ) {
        binding.nameTextView.text = product.name
        binding.root.setOnClickListener {
            onProductClick(product)
        }
        binding.favouriteButton.setOnClickListener {
            onFavouriteClick(product)
        }
    }
}

If a child button consumes the event, the root click will not normally run as well. That is usually desirable because tapping a favourite icon should not unexpectedly open a details page. Set a content description such as “Add ${product.name} to favourites” so TalkBack users receive meaningful feedback.

For an Australian retail app, a tap on an item might open a local pickup page, show delivery availability to Adelaide, or display prices including GST. Keep these decisions in the fragment or view model. The adapter should bind labels and report user actions, not contain shop rules or network calls.

Improve Updates And Accessibility

A basic List<Product> works for a static demonstration, but replacing the entire adapter list can produce inefficient updates. ListAdapter with DiffUtil calculates which rows changed and helps preserve smooth scrolling when search results or database records update.

class ProductDiff : DiffUtil.ItemCallback<Product>() {
    override fun areItemsTheSame(old: Product, new: Product): Boolean =
        old.id == new.id

    override fun areContentsTheSame(old: Product, new: Product): Boolean =
        old == new
}

When using a ListAdapter, call submitList(updatedProducts) instead of manually notifying every position. The click lambda remains the same, and the model ID remains dependable after sorting or filtering. Test taps after rotation, after a refresh, and when the list is empty.

Also consider privacy when recording click events. If your app sends browsing or purchase interactions to analytics services, explain that collection in the privacy notice and handle personal information according to the Australian Privacy Act 1988. A local marketplace may need extra care when a product click can be linked to an account, address, or order history.

Test Common Click And Scroll Cases

Run the app on different screen sizes and check that the entire row responds, the visual ripple appears, and a rapid double tap does not open duplicate destinations. Test a long list with fast scrolling, because RecyclerView reuses view holders and a misplaced listener can show the wrong product.

Verify behaviour after filtering, rotating the device, and returning from the details screen. On a phone used during a Melbourne tram ride or while comparing prices in a Brisbane supermarket, accidental taps and changing network conditions are realistic. Disable the item temporarily while a critical operation is running if duplicate submissions would be harmful.

Finally, test with TalkBack, a hardware keyboard, and larger font settings. Give buttons useful labels, keep touch targets comfortably sized, and ensure the selected state is communicated without relying only on colour. A well-designed RecyclerView click listener should feel immediate to the user while leaving data, navigation, and business logic in the correct Android component.