RecyclerView Basics

TrailLog's whole reason to exist is a list of logged trails, and that list needs to display comfortably whether the user has logged three trails or three hundred. RecyclerView is Android's standard widget for exactly that: displaying a scrolling list of items efficiently, no matter how long the list gets. This lesson builds the trail list from the ground up: the item layout, the Adapter, the ViewHolder, the LayoutManager, and handling a tap on a row.

Why "Recycler"? The Problem It Solves

If TrailLog naively created a brand-new row view for every single trail in the list, a user with three hundred logged trails would mean three hundred live view objects in memory — even though only eight or so are ever visible on screen at once. RecyclerView instead keeps a small pool of row views around, roughly enough to fill the visible screen plus a couple extra, and reuses them as the user scrolls: when a row scrolls off the top, its view isn't destroyed, it's handed back to display whatever new row is scrolling up from the bottom. This is why it matters for a list that could have hundreds of trails — memory use and scroll performance stay flat no matter how long the underlying list grows, because the number of live row views never grows past what the screen can actually show.

Piece 1: The Item Layout

First, a layout describing exactly one row in the list — a small card showing a trail's name and distance:

<!-- res/layout/item_trail.xml -->
<androidx.cardview.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    app:cardCornerRadius="8dp"
    app:cardElevation="2dp">

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

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

        <TextView
            android:id="@+id/itemTrailDistanceText"
            android:textColor="#666666"
            android:layout_marginTop="4dp"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </LinearLayout>
</androidx.cardview.widget.CardView>

This XML describes one card-shaped row — nothing here says anything about a list; it's purely the shape of a single item.

Piece 2: The ViewHolder

A ViewHolder wraps one inflated item_trail.xml row and holds onto references to its views, so they only need to be looked up once per row rather than every time that row is re-bound. Using View Binding here (see View Binding) gives type-safe references for free:

class TrailViewHolder(private val binding: ItemTrailBinding) :
    RecyclerView.ViewHolder(binding.root) {

    fun bind(trail: Trail) {
        binding.itemTrailNameText.text = trail.name
        binding.itemTrailDistanceText.text = "${trail.distanceKm} km"
    }
}

Piece 3: The Adapter

The Adapter is what connects your data (a list of Trail objects) to those row views. It has exactly three jobs: create a new row view when needed (onCreateViewHolder), fill an existing row with a particular trail's data (onBindViewHolder), and report how many items exist in total (getItemCount):

class TrailAdapter(
    private val trails: List<Trail>,
    private val onTrailClick: (Trail) -> Unit
) : RecyclerView.Adapter<TrailAdapter.TrailViewHolder>() {

    class TrailViewHolder(val binding: ItemTrailBinding) :
        RecyclerView.ViewHolder(binding.root)

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

    override fun onBindViewHolder(holder: TrailViewHolder, position: Int) {
        val trail = trails[position]
        holder.binding.itemTrailNameText.text = trail.name
        holder.binding.itemTrailDistanceText.text = "${trail.distanceKm} km"
        holder.binding.root.setOnClickListener { onTrailClick(trail) }
    }

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

onCreateViewHolder only runs when the pool of reusable rows needs a brand-new one — it inflates item_trail.xml once and wraps it in a TrailViewHolder. onBindViewHolder runs far more often, every time a row (new or recycled) needs to display a particular trail at a given position. Keeping the ViewHolder class nested inside the Adapter, as shown here, is a common convention since it's only ever meaningful in the context of this specific Adapter.

Piece 4: The LayoutManager

A RecyclerView on its own doesn't know how to arrange its items — that's the job of a LayoutManager. For a plain vertical list like TrailLog's trail list, LinearLayoutManager is the standard choice. If TrailLog ever wanted a grid of trail photo thumbnails instead of a single-column list, GridLayoutManager is the alternative — same Adapter, same ViewHolder, just a different arrangement of the same rows.

Wiring It All Together

Inside TrailListFragment, once the trails are loaded, connecting everything is just two lines:

binding.recyclerView.layoutManager = LinearLayoutManager(context)
binding.recyclerView.adapter = TrailAdapter(trails) { trail ->
    openTrailDetail(trail)
}

layoutManager tells the RecyclerView how to arrange rows; adapter tells it what data to show and how to build each row. Both must be set, or the list simply stays empty.

Handling an Item Click

Notice the TrailAdapter constructor above takes a lambda, onTrailClick: (Trail) -> Unit, alongside the list of trails. That lambda is called from inside onBindViewHolder, attached to each row's click listener (holder.binding.root.setOnClickListener { onTrailClick(trail) }). The Adapter itself doesn't know or care what happens on a click — it just reports which Trail was tapped, and the Fragment that created the Adapter decides what to do about it, in this case navigating to the trail's detail screen through the Navigation Component. This separation keeps the Adapter reusable: it never needs to change just because the destination of a tap changes.

Note: This Adapter, ViewHolder, and XML are correct and match how a real TrailLog project would be structured, but this website's own project doesn't run a live Android Studio build, so nothing here executes on the page itself.
Up next: This basic Adapter re-binds every visible row whenever the trail list changes at all. RecyclerView: DiffUtil & ListAdapter shows the more efficient, production-standard way to handle list updates.