Android Handle RecyclerView Click Event With LiveData

February 27, 2019
Propagate RecyclerView Click Event to Parent Activity/Fragment

There are many ways to do this, but my favourite way is using LiveData due to the loose coupling.

  • We create a ViewModel to keep val selectItemEvent = SingleLiveEvent<Quote?>()
  • SingleLiveEvent is actually a modified MutableLiveData to allow single consumption/observe only. The reason is to avoid double consumption/observe during configuration change/screen rotation.
  • ViewModel is passed into RecyclerView.Adapter for click event messaging (viewModel.selectItemEvent.value = item), while the parent activity/fragment can observe the event.

MyQuoteListFragment.kt

class MyQuoteListFragment : Fragment() {

    companion object {

        @JvmStatic
        fun newInstance() =
            MyQuoteListFragment().apply {
                arguments = Bundle().apply {
                    // putInt(ARG_COLUMN_COUNT, columnCount)
                }
            }
    }

    private lateinit var viewModel: MyQuoteListViewModel
    private lateinit var adapter: MyQuoteAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        arguments?.let {
            // columnCount = it.getInt(ARG_COLUMN_COUNT)
        }
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.myquote_list, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        viewModel = ViewModelProviders.of(this).get(MyQuoteListViewModel::class.java)

        val items = listOf(
            Quote("Premature optimization is the root of all evil", null),
            Quote("Any sufficiently advanced technology is indistinguishable from magic.", "Arthur C. Clarke"),
            Quote("Content 01", "Source"),
            Quote("Content 02", "Source"),
            Quote("Content 03", "Source"),
            Quote("Content 04", "Source"),
            Quote("Content 05", "Source")
        )

        adapter = MyQuoteAdapter(viewModel)
        adapter.replaceItems(items)
        list.adapter = adapter

        // listen to recyclerView click event
        viewModel.selectItemEvent.observe(this, Observer{
            if (it != null) {
                Timber.d("selected=${it.content}")
            }
        })
    }

    class MyQuoteAdapter(val viewModel: MyQuoteListViewModel) : RecyclerView.Adapter<MyQuoteAdapter.ViewHolder>() {
        private var items = listOf<Quote>()


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

        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            val item = items[position]

            holder.contentTextView.text = item.content
            holder.sourceTextView.text = item.source

            holder.itemView.setOnClickListener {
                // fire recyclerView click event
                viewModel.selectItemEvent.value = item
            }            
        }

        fun replaceItems(items: List<Quote>) {
            this.items = items
            notifyDataSetChanged()
        }

        override fun getItemCount(): Int = items.size

        inner class ViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView), LayoutContainer
    }
}

MyQuoteListViewModel.kt

class MyQuoteListViewModel: ViewModel() {
    internal val selectItemEvent = SingleLiveEvent<Quote?>()
}

SingleLiveEvent.kt, based on SingleLiveEvent.

class SingleLiveEvent<T> : MutableLiveData<T>() {

    private val pending = AtomicBoolean(false)
    
    @MainThread
    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        if (hasActiveObservers()) {
            Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
        }

        // Observe the internal MutableLiveData
        super.observe(owner, Observer<T> { t ->
            if (pending.compareAndSet(true, false)) {
                observer.onChanged(t)
            }
        })
    }

    @MainThread
    override fun setValue(t: T?) {
        pending.set(true)
        super.setValue(t)
    }

    /**
     * Used for cases where T is Void, to make calls cleaner.
     */
    @MainThread
    fun call() {
        value = null
    }

    companion object {
        private const val TAG = "SingleLiveEvent"
    }
}

NOTE: The disadvantage of SingleLiveEvent is that there can only be one observer. There is solution to support multiple observers, which I haven’t tried yet.

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.