androidandroid-recyclerviewlistadapteritemtouchhelperandroid-diffutils

notifyItemMoved() not working when migrated to ListAdapter Android


We have this feature in our App where we can drag recycler view Items up and down.

Inside onMove() of ItemTouchHelper.Callback() we call

adapter.onItemMove(source.adapterPosition, target.adapterPosition)

and the adapter code goes like this

override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
    Collections.swap(dataList, fromPosition, toPosition)
    notifyItemMoved(fromPosition, toPosition)
    return true
}

Now, earlier adapter class was extending RecyclerView.Adapter() and we have following method to update our list using DiffUtils

fun setDataList(feeds: List<User>): DiffUtil.DiffResult {
    val diffResult = DiffUtil.calculateDiff(ContentDiffCall(mFeeds, feeds))
    this.mFeeds.clear()
    this.mFeeds.addAll(feeds)
    return diffResult
}

Drag and drop were working fine with this.

But when we have extended our adapter class from ListAdapter, the already working functionality(drag and drop) is breaking. The first item in the recycler view is not dragging beyond the second item and also position item dragged is not getting updated.

Reverting ListAdapter implementation makes it work again.

Couldn't understand why is this not working when ListAdapter itself extends RecyclerView.Adapter.

Adapter class

class ContentListAdapter(
private val headerListener: HeaderClickListener?,
private val listener: ListItemClickListener?,
private val screen: Screen,
private val feedInteractor: FeedInteractionManager
) : ListAdapter<BaseUiModel, RecyclerView.ViewHolder>(ContentListDiffCall()), ContentTouchHelperAdapter {

private var dataList = ArrayList<BaseUiModel>()

init {
    setHasStableIds(true)
}

fun setDataList(data: List<BaseUiModel>) {
    dataList.clear()
    dataList.addAll(data)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    return when (viewType) {
        HFType.CONTENT_LIST_HEADER.ordinal -> ContentListHeaderViewHolder.getInstance(
            parent,
            headerListener,
            R.layout.layout_content_list_header,
            screen
        )
        HFType.SONG.ordinal -> SongViewHolder.getInstance(parent, listener)
        else -> throw RuntimeException("there is no type that matches the type $viewType ; make sure your using types correctly")

    }
}

override fun getItemViewType(position: Int): Int {
    return getItem(position).hfType.ordinal
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    val data = getItem(position)
    when (holder) {
        is SongViewHolder -> {
            if (data is SongUiModel) {
                holder.bindViews(data)
            }
        }
        is ContentListHeaderViewHolder -> {
            if (data is HeaderUiModel) {
                holder.bindViews(data)
            }
        }
    }
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList<Any>) {
    if (payloads.isEmpty()) {
        onBindViewHolder(holder, position)
        return
    }
    val data = getItem(position)
    when (holder) {
        is SongViewHolder -> {
            if (data is SongUiModel) {
                holder.bindViews(data, payloads)
            }
        }
        is ContentListHeaderViewHolder -> {
            if (data is HeaderUiModel) {
                holder.bindViews(data, payloads)
            }
        }
    }
}

override fun getItemId(position: Int): Long {
    val data = getItem(position)
    return data.hashCode().toLong()
}

override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
    Collections.swap(dataList, fromPosition, toPosition)
    notifyItemMoved(fromPosition, toPosition)
    return true
}

override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
    super.onViewAttachedToWindow(holder)
    if (holder is HomeFeedViewHolder<*>) {
        holder.onHolderAttachedInViewPort()
    }
}

}

interface ContentTouchHelperAdapter {
      fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
}

Code snippet inside fragment

private fun setUpRecyclerView() {
    rv_item_list.apply {
        layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
        recycledViewPool.setMaxRecycledViews(HFType.SONG.ordinal, 12)
     recycledViewPool.setMaxRecycledViews(HFType.CONTENT_LIST_HEADER.ordinal, 1)
    }

    val spaceItemDecorator = ContentListSpaceDecorator(25.dpToPx())
    rv_item_list.addItemDecoration(spaceItemDecorator)

    adapter = ContentListAdapter(this, this, this, screen, this)
    rv_item_list.adapter = adapter

    val callback = SimpleTouchListener(adapter, contentListViewModel)
    itemTouchHelper = ItemTouchHelper(callback)
    itemTouchHelper.attachToRecyclerView(rv_item_list)

  }

SimpleTouchListener class

class SimpleTouchListener(
    private val listAdapter: ContentListAdapter,
    private val viewModel: ContentListViewModel
) :
    ItemTouchHelper.Callback() {
    private var fromPosition: Int? = null
    private var toPosition: Int? = null

    override fun isLongPressDragEnabled(): Boolean {
        return false
    }

    override fun isItemViewSwipeEnabled(): Boolean {
        return false
    }

    override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
        var dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
        if (viewHolder is ContentListHeaderViewHolder) {
            dragFlags = 0
        }
        return makeMovementFlags(dragFlags, 0)         
    }

    override fun onMove(
        recyclerView: RecyclerView,
        source: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
        if (source.itemViewType != target.itemViewType) {
            return false
        }
        if (fromPosition == null) {
            fromPosition = source.adapterPosition
        }
        toPosition = target.adapterPosition
        listAdapter.onItemMove(source.adapterPosition, target.adapterPosition)
        return true
    }

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, i: Int) {
        // Notify the adapter of the dismissal
    }

    override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
        if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
            if (viewHolder is ItemTouchHelperViewHolder) {
                val itemViewHolder = viewHolder as ItemTouchHelperViewHolder?
                itemViewHolder?.onItemSelected()
            }
        }
        super.onSelectedChanged(viewHolder, actionState)
    }

    override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
        super.clearView(recyclerView, viewHolder)
        if (viewHolder is ItemTouchHelperViewHolder) {
            // Tell the view holder it's time to restore the idle state
            val itemViewHolder = viewHolder as ItemTouchHelperViewHolder
            itemViewHolder.onItemClear()
        }
        if (fromPosition != null && toPosition != null) {
            //This method updates the list ordering in DB 
            viewModel.onContentPositionChange(fromPosition, toPosition)
        }
        fromPosition = null
        toPosition = null
    }
}

we call setAdapterMethod from an observer on Success

private fun setAdapterData(contentList: List<BaseUiModel>?) {
    contentList?.let { it ->
        adapter.setDataList(it)
        adapter.submitList(it)
    }
}

Solution

  • ListAdapter updates its content through submitList function:

    /**
    * Submits a new list to be diffed, and displayed.
    * <p>
    * If a list is already being displayed, a diff will be computed on a background thread, which
    * will dispatch Adapter.notifyItem events on the main thread.
    *
    * @param list The new list to be displayed.
    */
    public void submitList(@Nullable List<T> list) {
        mDiffer.submitList(list);
    }
    

    Then you can just call:

    listAdapter.submitList(updatedList)