Android Data Binding for RecyclerView With LiveData (Kotlin)

October 27, 2018

Refer to Android Data Binding With LiveData (Kotlin) for the standard setup.

NOTE: Following tutorial on Android RecyclerView With Different Layout / View Type (Kotlin).

Sample code to load adapter and RecyclerView.

private fun setupUi() {
    // list is RecyclerView
    list.layoutManager = LinearLayoutManager(context)
    // using a ViewModel
    adapter = LocalListAdapter(viewModel)
    list.adapter = adapter
    val items = mutableListOf<ViewItem>().apply {
        add(ViewItem.AlbumView("I am album"))
        add(ViewItem.PinView("I am Pin"))
    }
    adapter.replaceItems(items)
}

This example consist of a RecyclerView with 2 type of view: AlbumView which need Data Binding, while PinView doesn’t.

sealed class ViewItem(val resource: Int) {
    class AlbumView(val name: String): ViewItem(R.layout.albumview_list_item_album)
    class PinView(val name: String): ViewItem(R.layout.albumview_list_item_pin)
}

Adapter class.

For LiveData to work, we need to call binding.setLifecycleOwner but RecyclerView is not a LifecycleOwner. So we need to implement ViewHolder as a LifecycleOwner.

NOTE: If you are using observable, then LifecycleOwner implementation is not necessary.

private class LocalListAdapter(val viewModel: AlbumViewViewModel) : RecyclerView.Adapter<LocalListAdapter.ViewHolder>() {
    private var items: List<ViewItem> = listOf()

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

    override fun getItemViewType(position: Int): Int {
        return items[position].resource
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return if (viewType == R.layout.albumview_list_item_album) {
            val inflater = LayoutInflater.from(parent.context)
            val binding = AlbumviewListItemAlbumBinding.inflate(inflater, parent, false)
            // val binding = DataBindingUtil.inflate<AlbumviewListItemAlbumBinding>(inflater, R.layout.albumview_list_item_album, parent, false)
            val viewHolder = BindingViewHolder(binding.root, binding)
            // suspected cannot mix observable with LiveData with Observable
            binding.setLifecycleOwner(viewHolder)
            return viewHolder

        } else {
            val view = LayoutInflater.from(parent.context)
                    .inflate(viewType, parent, false)
            ViewHolder(view)
        }
    }

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

        when (item) {
            is ViewItem.AlbumView -> {
                if (holder is BindingViewHolder) {
                    holder.apply {
                        viewModel.liveItem.name.value = item.name
                        binding.viewModel = viewModel

                        // optional observer to update name if it changes (2-way binding through EditText)
                        viewModel.liveItem.name.observe(this, Observer {
                            it?.also {
                                item.name = it
                            }
                        })
                    }
                }
            }
            is ViewItem.PinView -> {
                // do something 
            }
        }
    }

    override fun onViewAttachedToWindow(holder: ViewHolder) {
        super.onViewAttachedToWindow(holder)
        if (holder is BindingViewHolder) {
            holder.markAttach()
        }
    }

    override fun onViewDetachedFromWindow(holder: ViewHolder) {
        super.onViewDetachedFromWindow(holder)

        if (holder is BindingViewHolder) {
            holder.markDetach()
        }
    }

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

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

    inner class BindingViewHolder(override val containerView: View, val binding: AlbumviewListItemAlbumBinding) : ViewHolder(containerView), LifecycleOwner {
        private val lifecycleRegistry = LifecycleRegistry(this)

        init {
            lifecycleRegistry.markState(Lifecycle.State.INITIALIZED)
        }

        fun markAttach() {
            // Lifecycle.State.CREATED doesn't work for this case
            // lifecycleRegistry.markState(Lifecycle.State.CREATED)
            lifecycleRegistry.markState(Lifecycle.State.STARTED)
            // lifecycleRegistry.markState(Lifecycle.State.RESUMED)
        }

        fun markDetach() {
            lifecycleRegistry.markState(Lifecycle.State.DESTROYED)
        }

        override fun getLifecycle(): Lifecycle {
            return lifecycleRegistry
        }
    }
}

Layout file albumview_list_item_album.xml will generate AlbumviewListItemAlbumBinding.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="viewModel" type="com.luasoftware.pixpin.view.AlbumViewViewModel"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        >

        <EditText
            android:id="@+id/albumNameEditText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@={viewModel.liveItem.name}"
            />

        <TextView
            android:id="@+id/albumNameTextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{viewModel.liveItem.name}"
            />
    </LinearLayout>
</layout>

ViewModel

// I remove my dagger2 injection code
// class AlbumViewViewModel @Inject constructor(private val dataSource: AlbumDao): ViewModel() {
class AlbumViewViewModel : ViewModel() {
    inner class LiveItem {
        val name = MutableLiveData<String>()
    }
}
This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.