Simplify your adapters - lambdas in data classes

Data classes are a Kotlin feature that allow us to write clean, simple classes that are commonly used to model data, such as API requests/responses and application state. They have have automatically derived equals(), hashCode() and toString() functions.

This is an example of a simple data class Item that holds an id, title and subtitle:

    data class Item(
        val id: String,
        val title: String,
        val subtitle: String
    )

On Android we can use a data class to represent an item in a list. This list of data can be used by an adapter to render a RecyclerView:

val items = listOf(
            Item(id = "id 1", title = "title 1", subtitle = "title 1"),
            Item(id = "id 2", title = "title 2", subtitle = "title 2")
        )

Adapter click listeners

Setting a click listener to an adapter usually requires bindings for Presenter/ViewModel <-> View <-> Adapter.

We can simplify this by using a lambda inside a data class.

Modifying our original data class to add a lambda:

val listener: (String) -> Unit

But we don’t want to pass a new lambda into every Item as this would break equality checks which are important for performance when rendering a large list. This is because the DiffUtil used with RecylerView checks item contents for equality, commonly using isEquals().

By using a method reference in the data class we can point to the same function for every Item, this ensures that lambda usage doesn’t break our equality checks and gives us a simple way of handling callbacks using data classes.

An example of this is a ViewModel that emits a list of items as state, containing a callback to the ViewModel for clicks:

class HomeViewModel : ViewModel() {

    private var _data = MutableLiveData<State>()
    val data: LiveData<State> = _data

    init {
        val items = listOf(
                Item(id = "id 1", title = "title 1", subtitle = "subtitle 1", listener = ::onClick),
                Item(id = "id 2", title = "title 2", subtitle = "subtitle 2", listener = ::onClick)
        )
        _data.value = State(items = items)
    }

    private fun onClick(id: String) {
        Timber.d("Do something with $id")
    }

    data class State(val items: List<Item> = emptyList())
}

data class Item(
        val id: String,
        val title: String,
        val subtitle: String,
        val listener: (String) -> Unit
)

And the Activity that observes the ViewModel.State

class HomeActivity : AppCompatActivity() {

    private val adapter = Adapter()
    private val viewModel = HomeViewModel()

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

        homeRecyclerView.adapter = adapter

        viewModel.data.observe(this, Observer<HomeViewModel.State> { state ->
            adapter.submitList(state.items)
        })

    }

    class Adapter : ListAdapter<Item, Adapter.ViewHolder>(ItemDiff) {

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
            return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false))
        }

        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            val item = getItem(position)
            holder.container.setOnClickListener { item.listener(item.id) }
            holder.titleTextView.text = item.title
            holder.subtitleTextView.text = item.subtitle
        }

        class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
            val container: LinearLayout = view.itemContainer
            val titleTextView: TextView = view.title
            val subtitleTextView: TextView = view.subtitle
        }

        object ItemDiff : DiffUtil.ItemCallback<Item>() {
            override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
                return oldItem == newItem
            }

        }
    }
}