Search code examples
androidkotlindata-bindingandroid-databinding

RecyclerView data binding issue with item ViewModel


In my code I'm using two view models: one for the list of items used in a RecyclerView and one for the item itself, which I am binding to an item detail view. Once the view is created, I'm having issues making any updates from the item detail view model to the view. So in the code below, when the button is clicked, the onButtonClick() function in the ItemViewModel is called but text update in the item_card view does not update.

Any ideas greatly appreciated.

Thanks!

MainActivity.kt:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private lateinit var itemListVM: ItemListViewModel

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

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.lifecycleOwner = this

        setupBindings()
    }

private fun setupBindings() {

    binding.lifecycleOwner = this

    // Create VM
    itemListVM = ViewModelProviders.of(this).get(ItemListViewModel::class.java)
    binding.itemListVM = itemListVM

    setupRecyclerView()

}

// Set up RecyclerView
private fun setupRecyclerView() {
    val recyclerView = binding.itemListRecyclerview
    val layoutManager = LinearLayoutManager(this)
    recyclerView.layoutManager = layoutManager

     // Set adapter
    recyclerView.adapter = itemListVM.getAdapter()

    // Create some data
    var itemList = mutableListOf<ItemModel>()
    for (i in 0..5){
        var item = ItemModel()
        itemList.add(item)
    }

    itemListVM.setItemList(itemList)
}

ItemModel.kt:
class ItemModel{
   var text = ""
}

 ItemViewModel.kt:
 class ItemViewModel: ViewModel(){
    var text = MutableLiveData<String>()
    lateinit var itemModel : ItemModel
    var position : Int = -1

    init{
        text.value = "init"
    }

    fun setItem(itemModel: ItemModel){
        this.itemModel = itemModel
        text.value = "set model"
    }

    fun onButtonClick(){
        Log.d("ItemViewModel", "position: $position")
        text.value = "button click"
    }
}

ItemListViewModel.kt:
class ItemListViewModel: ViewModel(){

    var itemVMMap = mutableMapOf<Int, ItemViewModel>()
    var itemListAdapter : ItemListAdapter? = null

    init {
        itemListAdapter = ItemListAdapter(this)
    }

    fun getAdapter(): ItemListAdapter? {
        return itemListAdapter
    }

    fun setItemList(itemList: List<ItemModel>){
        itemListAdapter?.setItemList(itemList)
    }

    fun getItemAt(position: Int?): ItemModel? {
        val itemList = itemListAdapter?.getItemList()

        if (itemList != null &&
            position != null &&
            itemList.size > position){
            return itemList.get(position)
        }
        else {
            return null
        }
    }

    fun getItemVMAtPosition(position: Int?):ItemViewModel?{
        // Check if VM exists
        var itemVM: ItemViewModel? = null
        if (itemVMMap.contains(position)){
            itemVM = itemVMMap[position]
        }
        else {
            // Create a new VM
            itemVM = ItemViewModel()
            itemVM.position = position!!

            // Get item
            val item = getItemAt(position)

            item?.let{
                itemVM?.setItem(it)
            }

            itemVMMap[position!!] = itemVM
        }

        return itemVM
    }
}

ItemListAdapter.kt:
class ItemListAdapter
internal constructor(listViewModel : ItemListViewModel) : RecyclerView.Adapter<ItemListAdapter.GenericViewHolder>() {

    private var itemList: List<ItemModel>? = null
    private var itemListVM: ItemListViewModel? = null
    private var layoutId: Int = 0

    init {
        this.layoutId = layoutId
        this.itemListVM = listViewModel
    }

    private fun getLayoutIdForPosition(position: Int): Int {
        return layoutId
    }

    override fun getItemCount(): Int {
        return itemList?.size ?: 0
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenericViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding = DataBindingUtil.inflate<ViewDataBinding>(
            layoutInflater,
            com.example.test2.R.layout.item_card,
            parent,
            false
        ) as com.example.test2.databinding.ItemCardBinding

        return GenericViewHolder(binding)
    }

    override fun onBindViewHolder(holder: GenericViewHolder, position: Int) {
        if (itemListVM != null) {
            val itemVM = itemListVM!!.getItemVMAtPosition(position)
            holder.bind(itemVM!!, position)
        }
    }

    override fun getItemViewType(position: Int): Int {
        return getLayoutIdForPosition(position)
    }

    // View Holder Definition
    inner class GenericViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) {

        fun bind(viewModel: ItemViewModel, position: Int?) {
            binding.setVariable(BR.itemListVM, itemListVM)
            binding.setVariable(BR.position, position)
            binding.setVariable(BR.itemVM, viewModel)

            binding.executePendingBindings()
        }
    }

    // Setters
    fun setItemList(itemList: List<ItemModel>?) {
        this.itemList = itemList
        notifyDataSetChanged()
    }

    // Getters
    fun getItemList(): List<ItemModel>? {
        return itemList
    }
}

activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable name="itemListVM" type="com.example.test2.ItemListViewModel"/>
    </data>
    <androidx.coordinatorlayout.widget.CoordinatorLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".MainActivity">

            <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/item_list_recyclerview"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:scrollbars="vertical">
            </androidx.recyclerview.widget.RecyclerView>
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

item_card.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <import type="android.view.View" />
        <variable name="itemListVM" type="com.example.test2.ItemListViewModel"/>
        <variable name="position" type="java.lang.Integer" />
        <variable name="itemVM" type="com.example.test2.ItemViewModel"/>
    </data>
    <LinearLayout
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                    android:orientation="horizontal"
                    android:layout_margin="12dp">
        <TextView android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:text="@{itemVM.text}"/>
        <Button android:layout_height="wrap_content"
                android:layout_width="wrap_content"
                android:paddingLeft="12dp"
                android:onClick="@{() -> itemVM.onButtonClick()}"
                android:text="Click Me"/>
    </LinearLayout>
</layout>

Solution

  • This is just because you are using a mutable property, but not notifying your view somehow that your value is being updated. for binding them directly you should make it an observable and then attach it with view in databinding or you should notify the updates you are doing on that variable.

    public class Foo {
        public final ObservableField<String> baz = new ObservableField<>();
    }
    

    however on view side do this,

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{foo.baz}"/>
    

    and while changing the value you can do it like that -

    baz.set("changed value") and it will automatically update your view.