3

Context :
MainActivity launches, create a list of ItemObject, and call ListFragment. When the user clicks on button (which is in MainActivity), it modify the list, then call the listFragment to update it.

Issues found :
In the update function on listFragment, newList and oldList are the same.

Here is my code.

MainActivity

class MainActivity : AppCompatActivity() {

    private val viewModel = MainViewModel()

    private var button : FloatingActionButton? = null

    private val listFragment = ListFragment.newInstance()

    var list = ArrayList<ItemObject>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        binding.viewModel = viewModel

        button = binding.button
        button?.setOnClickListener { modifyList() }

        createItemsList()
    }

    /**
     *  This creates a list of 10 ItemObject, that will contains `i` as `randomInt` and "TEST $i" as `title`, then call
     *  setListFragment().
     */
    private fun createItemsList() {
        val itemsList = ArrayList<ItemObject>()
        var i = 0
        while (i < 10) {
            itemsList.add(ItemObject().apply { this.randomInt = i ; this.title = "TEST $i" })
            i++
        }
        list = itemsList
        setListFragment()
    }

    /**
     *  Set listFragment inside content.
     */
    private fun setListFragment() {
        supportFragmentManager.beginTransaction().replace(R.id.content, listFragment).commit()
    }

    /**
     *  Triggered when the user clicks on the FloatingActionButton. Will modify each even item, add 2 to its `randomInt`
     *  and set its `title` to "MODIFIED $randomInt".
     */
    private fun modifyList() {
        list.forEach {
            if (it.randomInt % 2 == 0) {
                it.randomInt += 2
                it.title = "MODIFIED ${it.randomInt}"
            }
        }
        if (listFragment.isAdded) {
            listFragment.updateList(list)
        }
    }

    inner class MainViewModel
}

And ListFragment :

class ListFragment : Fragment() {

    private val viewModel = ListViewModel()
    private val listAdapter = ListAdapter()

    private var listRv : RecyclerView? = null

    private var list = ArrayList<ItemObject>()

    override fun onCreateView(inflater: LayoutInflater, parent: ViewGroup?, savedInstanceState: Bundle?): View? {
        val binding = DataBindingUtil.inflate<FragmentListBinding>(inflater, R.layout.fragment_list, parent, false)
        binding.viewModel = viewModel

        listRv = binding.listRv

        list = (activity as MainActivity).list

        setList()

        return (binding.root)
    }

    /**
     *  Sets up the RecyclerView and set the list inside it.
     */
    private fun setList() {
        listRv?.layoutManager = LinearLayoutManager(context)
        listRv?.adapter = listAdapter
        listRv?.post { listAdapter.setData(list) }
    }

    /**
     *  Triggered by MainActivity when the user clicks on the button and the list is modified. Will call update() method
     *  from adapter.
     */
    fun updateList(newList : ArrayList<ItemObject>) {
        listAdapter.update(newList)
    }

    companion object {
        fun newInstance() : ListFragment = ListFragment()
    }

    inner class ListViewModel

    inner class ItemDiff : DiffUtil.Callback() {

        private var old = ArrayList<ItemObject>()
        private var new = ArrayList<ItemObject>()

        fun setLists(old : ArrayList<ItemObject>, new : ArrayList<ItemObject>) {
            this.old = old
            this.new = new
        }

        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            println("ARE ITEMS ${old[oldItemPosition].title} THE SAME ? ${(old[oldItemPosition] == new[newItemPosition])}")
            return (old[oldItemPosition] == new[newItemPosition])
        }

        override fun getOldListSize(): Int = old.size

        override fun getNewListSize(): Int = new.size

        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            println("ARE ITEMS ${old[oldItemPosition].title} CONTENTS THE SAME ? ${(old[oldItemPosition].title == new[newItemPosition].title
                && old[oldItemPosition].randomInt == new[newItemPosition].randomInt)}")
            return (old[oldItemPosition].title == new[newItemPosition].title
                && old[oldItemPosition].randomInt == new[newItemPosition].randomInt)
        }

        override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
            val oldItem = old[oldItemPosition]
            val newItem = new[newItemPosition]
            val bundle = Bundle()
            if (oldItem.title != newItem.title) {
                println("SHOULD ADD NEW STRING ${newItem.title}")
                bundle.putString("title", newItem.title)
            }
            if (oldItem.randomInt != newItem.randomInt) {
                println("SHOULD ADD NEW INT ${newItem.randomInt}")
                bundle.putInt("randomInt", newItem.randomInt)
            }
            return (bundle)
        }
    }

    inner class ListAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

        private var items = ArrayList<ItemObject>()

        fun setData(list : ArrayList<ItemObject>) {
            items = list
            notifyDataSetChanged()
        }

        /**
         *  @param new
         *  Triggered when the list is modified in the parent activity. Uses DiffUtil to update the list.
         */
        fun update(new : ArrayList<ItemObject>) {
            println("///// IN UPDATE ; WILL PRINT OLD AND NEW LIST /////")
            items.forEach { println("OLD ITEM ${it.title}") }
            new.forEach { println("NEW ITEM ${it.title}") }
            println("///// PRINT END /////")
            val diffCallback = ItemDiff()
            diffCallback.setLists(old = items, new = new)
            val diffResult = DiffUtil.calculateDiff(diffCallback)
            diffResult.dispatchUpdatesTo(this)
            items = new

        }

        override fun onCreateViewHolder(parent: ViewGroup, position: Int): RecyclerView.ViewHolder {
            return (ItemViewHolder(DataBindingUtil.inflate<ItemBinding>(LayoutInflater.from(parent.context),
            R.layout.item, parent, false).apply {
                viewModel = ItemViewModel()
            }))
        }

        override fun getItemCount(): Int = items.size

        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
            (holder as ItemViewHolder).setData(items[position])
        }

        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList<Any>) {
            println("IN ON BIND VIEWHOLDER ; PAYLOAD SIZE = ${payloads.size}")
            if (payloads.isEmpty())
                super.onBindViewHolder(holder, position, payloads)
            else {
                val bundle = payloads[0] as Bundle
                if (bundle.size() != 0) {
                    val name = bundle.getString("name")
                    val randomInt = bundle.getInt("randomInt")
                    if (name != null)
                        (holder as ItemViewHolder).setName(name)
                    (holder as ItemViewHolder).setRandomInt(randomInt)
                }
            }
        }

        inner class ItemViewHolder(private val binding : ItemBinding) : RecyclerView.ViewHolder(binding.root) {

            fun setData(item : ItemObject) {
                binding.viewModel?.setData(item)
            }

            fun setRandomInt(newInt : Int) {
                binding.viewModel?.setRandomInt(newInt)
            }

            fun setName(newName : String) {
                binding.viewModel?.setTitle(newName)
            }
        }

        inner class ItemViewModel {

            val title = ObservableField<String>("")
            val randomInt = ObservableField<String>("")

            fun setData(item : ItemObject) {
                setRandomInt(item.randomInt)
                setTitle(item.title)
            }

            fun setRandomInt(newInt : Int) {
                randomInt.set(newInt.toString())
            }

            fun setTitle(newName : String) {
                title.set(newName)
            }
        }

    }
}

Here is my ItemObject class :

class ItemObject {

    var title = ""
    var randomInt = 0
}

What is strange is that i NEVER modify my list inside ListFragment. So how is that possible that when the list is updated, my debug println show me that old and new lists are the same ?

Mathieu
  • 1,435
  • 3
  • 16
  • 35
  • Can you try to make a minimal example that reproduces the issue? I doubt you will get much help when the example is so big. – marstran Jun 07 '19 at 10:12
  • how does your ItemObject class look like? – r2rek Jun 07 '19 at 11:54
  • @marstran Actually I made this whole application just to test this issue... In the original app it would be much bigger to explain. – Mathieu Jun 07 '19 at 12:46
  • @r2rek I have edit my question, but I doubt this is useful : The problem is not the object, but that both lists seems to be updated at once. – Mathieu Jun 07 '19 at 12:46
  • 4
    Sorry if this is old news, but it looks like you are 'passing by reference' on the list. So the list that you are modifying is the same list that you use to pass to the Adapter. You want to create two completely separate lists in memory for DiffUtil to work properly. It's easy to overlook and I've done this before. – zuko Dec 10 '19 at 19:46

0 Answers0