SG Sanjay Gangwar

Article

Generating RecyclerView Adapters and ViewHolders in Android Studio

Every list screen in an Android app needs the same two classes. This is a small plugin that writes them, and the reasoning behind how it does it.

The problem

Two files, every single list

A RecyclerView needs an adapter and a view holder. Neither is complicated, and that is exactly the problem: they are long enough to be tedious and similar enough that you stop reading them. On a portfolio of apps, that is the same two files written over and over, with the same four or five names threaded through both.

Android Studio already has File and Code Templates, and for a single class it is fine. It falls short here for one specific reason: it generates one file at a time and has no idea that the two files refer to each other. The adapter's generic parameter is the view holder's class name; the view holder's constructor takes the adapter's listener interface. A per-file template cannot fill both sides, so you create two files and then hand-edit the references — which is most of the work you were trying to avoid.

So the tool needed to be a plugin rather than a template: something that asks once and writes both files with the cross-references already correct.

What it does

One dialog, both files

Right-click a package, pick New › Recycler Adapter and ViewHolder, and type one name. Typing Product fills in the rest:

  • Adapter class — ProductAdapter
  • ViewHolder class — ProductViewHolder
  • Model class — Product
  • View binding class — ItemProductBinding, from the item_product.xml naming almost every row layout uses

Each derived field stops following the moment you edit it by hand, so the guesses never fight you. Typing ProductAdapter instead of Product does not produce ProductAdapterAdapter — the suffix is stripped before the others are derived.

Output

The generated code

Given the model com.app.data.Product and the binding com.app.databinding.ItemProductBinding, dropped into com.app.ui.product, this is the adapter it writes:

package com.app.ui.product

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.app.data.Product
import com.app.databinding.ItemProductBinding

class ProductAdapter(
    private val context: Context,
    private val listener: OnClickListener,
    private val flag: Boolean
) : RecyclerView.Adapter<ProductViewHolder>() {

    interface OnClickListener {
        fun onItemClicked(id: Product)
    }

    private val items = ArrayList<Product>()

    fun setItems(items: ArrayList<Product>) {
        this.items.clear()
        this.items.addAll(items)
        notifyDataSetChanged()
    }

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

        return ProductViewHolder(context, binding, listener, flag)
    }

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

    override fun getItemCount(): Int {
        return items.size
    }

}
ProductAdapter.kt, generated verbatim — no placeholders left to fill in.

And the view holder, which already knows the adapter's listener type:

package com.app.ui.product

import android.content.Context
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.app.R
import com.app.data.Product
import com.app.databinding.ItemProductBinding

class ProductViewHolder(
    private val context: Context,
    private val bind: ItemProductBinding,
    private val listener: ProductAdapter.OnClickListener,
    private val flag: Boolean
) : RecyclerView.ViewHolder(bind.root), View.OnClickListener {

    private lateinit var items: Product

    init {
        bind.card.setOnClickListener(this)
    }

    fun bind(data: Product) {
        this.items = data
        //bind data with ui
    }

    override fun onClick(p0: View) {
        when (p0.id) {
            R.id.card -> {
                if (flag) {
                    listener.onItemClicked(items)
                }
            }
        }
    }
}
ProductViewHolder.kt. The bind.card click target and the empty bind() body are the two places you actually write code.

Details

Imports, and the R trick

The model and view binding fields accept fully qualified names. When you give one, the plugin shortens it in the body and adds the import, skipping anything already in the target package. Small thing, but it is the difference between generated code that compiles and generated code with four red lines at the top.

The R import is the interesting one. A view holder that references R.id.card needs R imported, but R lives in the module's own namespace and nothing in the dialog tells you what that is. Rather than depend on the Android plugin's APIs to find out, the plugin derives it from the view binding package: view bindings always live in <module>.databinding, so com.app.databinding.ItemProductBinding means R is com.app.R. When the binding is not qualified, the import is simply left out rather than guessed at.

Variables that render empty do not leave ragged blank lines behind, and the file always ends with exactly one newline — the sort of detail that is invisible when it works and irritating when it does not.

Templates

The templates are yours

My adapter is not your adapter. Under Settings › Tools › FastCode Templates both bundled templates are editable, and you can add your own — a new template shows up in the New menu straight away, with no restart.

Only your changes are stored, not a copy of the whole template. A template you never touched keeps tracking updates to the plugin; one you edited stays edited, with a reset button to put it back. Templates understand these variables:

${PACKAGE_NAME}       package of the target directory
${CLASS_NAME}         the class this file declares
${ADAPTER_CLASS}      adapter class name
${VIEW_HOLDER_NAME}   view holder class name
${MODEL_CLASS}        model type, simple name
${VIEWBINDING_CLASS}  view binding type, simple name
${EXTRA_IMPORTS}      imports for whichever types were given qualified
${R_IMPORT}           import <app>.R, or empty when it cannot be derived

Matching is case-insensitive and the common alternate spellings resolve, so ${VIEW_BINDING} and ${VIEWHOLDER_CLASS} work too. A variable the plugin does not recognise is left in the file exactly as written and reported in a balloon, so a typo is visible instead of silently blank.

Install

Get it

It is a free, unsigned build, not a JetBrains Marketplace listing — I wrote it for my own apps and it is small enough that a download is simpler than a store page.

  1. Download FastCode-1.0.0.zip
  2. In the IDE: Settings › Plugins › ⚙ › Install Plugin from Disk…
  3. Pick the zip. It loads dynamically, so no restart — the entries appear under New immediately.

Built against the 2024.3 platform with no upper version bound, and verified compatible with IntelliJ IDEA 2024.3, 2025.1 and 2025.2 as well as Android Studio 2026.1. If it misbehaves in your setup, tell me — it is a small enough tool that I can usually fix it the same day.