Как сохранить порядок элементов RecyclerView (после перетаскивания)?

#android #kotlin #android-recyclerview

#Android #kotlin #android-recyclerview

Вопрос:

У меня есть простой RecyclerView, и я просто добавил функцию перетаскивания с помощью ItemTouchHelper.Callback . В clearView функции я указываю своей модели представления изменить живые данные элементов, чтобы я мог сохранить их позже. Чтобы свести к минимуму вызовы сервера, я сохраняю список в Firestore в двух случаях:

  1. Метод ViewModel onCleared
  2. При сохранении любого другого изменения (например, добавления / обновления / удаления элемента)

Первый случай работает хорошо, но во втором я сталкиваюсь со странной проблемой: я получаю другую анимацию перетаскивания и в итоге получаю как старые, так и новые значения для разных элементов (одно переопределяет неизмененный элемент) и в беспорядочном порядке. Когда я перемещаюсь по фрагменту и возвращаюсь обратно, порядок и значения соответствуют назначению (потому что они были правильно сохранены в Firestore и правильно перезагружены). Таким образом, я думал, что проблема заключалась в том, что LiveData была перезагружена, но теперь я вижу, что аналогичная проблема возникает, когда я переупорядочиваю, а затем прокручиваю, чтобы переупорядоченные элементы не отображались (когда я вернулся, элемент в конечной позиции переопределяется перетаскиваемым элементом, но элемент в начальная позиция также является перетаскиваемым элементом). Есть ли способ сообщить адаптеру сохранять порядок после каждого удаления? Потому что я думаю, что проблема возникает, когда он пытается отменить порядок, когда изменяются LiveData или элементы не видны… Кроме того, я думаю, что не до конца понимаю весь процесс, поэтому я был бы рад получить некоторые объяснения 🙂

Мой код:

 class TalesTouchCallback(private val adapter: TalesAdapter) : ItemTouchHelper.Callback() {

    // Keeps the starting position of the dragged item
    var oldPosition: Int? = null

    override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder)
            : Int {
        // Specify the directions of movement
        val dragFlags =
            if (viewHolder.itemViewType == TalesAdapter.ITEM_VIEW_TYPE_ITEM)
                ItemTouchHelper.UP or ItemTouchHelper.DOWN
            else ItemTouchHelper.ACTION_STATE_IDLE  // The header can't be moved
        return makeMovementFlags(dragFlags, 0)
    }

    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
        // Notify the adapter that an item is moved from x position to y position
        if (target.adapterPosition > 0)     // Allow dragging over the header without interruption
            adapter.notifyItemMoved(viewHolder.adapterPosition, target.adapterPosition)
        return true
    }

    override fun isLongPressDragEnabled(): Boolean {
        return true
    }

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {

    }

    override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
        super.onSelectedChanged(viewHolder, actionState)
        // Handle action state changes
        viewHolder?.let { oldPosition = it.adapterPosition }
    }

    override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
        super.clearView(recyclerView, viewHolder)
        oldPosition?.let {
            if (it != viewHolder.adapterPosition) adapter.onRowMoved(
                it,
                viewHolder.adapterPosition
            )
            oldPosition = null
        }
    }
}


class TalesAdapter(private val listener: TalesListener) :
    ListAdapter<TalesAdapter.DataItem, RecyclerView.ViewHolder>(TalesDiffCallback()) {

...

    fun onRowMoved(fromPosition: Int, toPosition: Int) =
        // Only the adapter knows about the header - so the positions in the actual list are -1
        if (toPosition > 0) listener.onMoved(fromPosition - 1, toPosition - 1) else null

    class TalesDiffCallback : DiffUtil.ItemCallback<DataItem>() {
        override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
            return oldItem == newItem
        }
    }

    sealed class DataItem {
        abstract val id: String

        data class TaleItem(val model: TaleItemModel) : DataItem() {
            override val id = model.id
        }

        object Header : DataItem() {
            override val id = ""
        }
    }

    companion object {
        const val ITEM_VIEW_TYPE_HEADER = 0
        const val ITEM_VIEW_TYPE_ITEM = 1
    }
}


class TalesViewModel : BaseViewModel(), TalesAdapter.TalesListener {
    // The user's tales list 
    val tales: LiveData<MutableList<TaleItemModel>> = Database.userTales

    // The tale currently being edited or deleted
    private var clickedTale: TaleItemModel? = null

    // Did the user change the tales order?
    private var wasOrderChanged = false

    // Types of supported events
    sealed class EventType() : Event {
        class ShowEditTaleDialog(val taleTitle: String) : EventType()
        class ShowDeleteTaleDialog(val taleTitle: String) : EventType()
        object ShowAddTaleDialog : EventType()
    }

    fun onAdd() = postEvent(EventType.ShowAddTaleDialog)

    override fun onEdit(taleItem: TaleItemModel) {
        clickedTale = taleItem
        postEvent(EventType.ShowEditTaleDialog(taleItem.title))
    }

    override fun onDelete(taleItem: TaleItemModel) {
        clickedTale = taleItem
        postEvent(EventType.ShowDeleteTaleDialog(taleItem.title))
    }

    fun clearClickedTale() {
        clickedTale = null
    }

    fun addTale(title: String) = Database.createTale(title)?.withProgressBar()

    override fun onMoved(fromPosition: Int, toPosition: Int) {
        // Move the tale to it's right place in the LiveData list
        tales.value?.let {
            if (fromPosition < toPosition) {
                for (i in fromPosition until toPosition) Collections.swap(it, i, i   1)
            } else {
                for (i in fromPosition downTo toPosition   1) Collections.swap(it, i, i - 1)
            }
        }
        wasOrderChanged = true
    }

    fun updateTale(title: String) =
    // Copy is needed because if we change the original item, adapter's new list and old list would
        // be the same and it will not refresh. Thus a copy is needed.
        clickedTale?.copy(title = title)
            ?.let {
                Database.updateTale(it)?.withProgressBar()
                    ?.addOnCompleteListener { clearClickedTale() }
            }

    fun deleteTale() =
        clickedTale
            ?.let {
                Database.deleteTale(it.id)?.withProgressBar()
                    ?.addOnCompleteListener { clearClickedTale() }
            }

    override fun onCleared() {
        super.onCleared()
        if (wasOrderChanged) Database.saveTalesOrder()?.withProgressBar()
    }
}
 

( Database Класс вызывает функции Firestore. При настройке / удалении он также берет список из Database.userTales LiveData, поэтому сохраняет правильный порядок)

Любая помощь будет принята 🙂