Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to use drag and drop on recycler view using firebase realtime database

Im trying to implement the drag and drop feature to firebase recycler view. There is not enough information in the docs for this implementation. Im assuming i have to use onchildmoved for the event listener but i do not know how to reorder the data.

like image 247
Smeekheff Avatar asked Jun 28 '16 19:06

Smeekheff


2 Answers

Here I post solution that worked for me. It's based on @prodaea's answer, which has right approach but is not fully functional. It's missing firebase database update and additional check against ChangeEventType.CHANGED to avoid the animation interruption when firebase updates.

Code lives inside adapter which extends FirebaseRcyclerAdapter and implements interface with a single method onItemMove(fromPosition: Int, toPosition: Int): Boolean:

override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {

    snapshots[fromPosition].sort = toPosition.toDouble()
    snapshots[toPosition].sort = fromPosition.toDouble()

    // update recycler locally to complete the animation
    notifyItemMoved(fromPosition, toPosition)

    updateFirebase(fromPosition, toPosition)

    return true
}

private fun updateFirebase(fromPos: Int, toPos: Int) {

    // flag which prevents callbacks from interrupting the drag animation when firebase database updates
    hasDragged = true

    val firstPath = getRef(fromPos).getPath()
    val secondPath = getRef(toPos).getPath()

    Log.d(_tag, "onItemMove: firstPath: $firstPath, secondPath: $secondPath")
    val updates = mapOf(
            Pair(firstPath + "/sort", snapshots[fromPos].sort),
            Pair(secondPath + "/sort", snapshots[toPos].sort))
    getRef(fromPos).root.updateChildren(updates)

    Log.d(_tag, "updateFirebase, catA: \"${snapshots[fromPos]}\", catB: \"${snapshots[toPos]}\"")
}

private fun DatabaseReference.getPath() = toString().substring(root.toString().length)

override fun onChildChanged(type: ChangeEventType,
                            snapshot: DataSnapshot,
                            newIndex: Int,
                            oldIndex: Int) {

    Log.d(_tag, "onChildChanged:else, type:$type, new: $newIndex, old: $oldIndex, hasDragged: $hasDragged, snapshot: $snapshot")

    when (type) {
        // avoid the drag animation interruption by checking against 'hasDragged' flag
        ChangeEventType.MOVED -> if (!hasDragged) {
            super.onChildChanged(type, snapshot, newIndex, oldIndex)
        }
        ChangeEventType.CHANGED -> if (!hasDragged) {
            super.onChildChanged(type, snapshot, newIndex, oldIndex)
        }
        else -> {
            super.onChildChanged(type, snapshot, newIndex, oldIndex)
        }
    }
}

override fun onDataChanged() {
    hasDragged = false
    Log.d(_tag, "onDataChanged, hasDragged: $hasDragged")
}
like image 97
AlexKost Avatar answered Sep 19 '22 02:09

AlexKost


I had quite a bit of confusion trying to get this to work for myself on Firebase UI 3.1.0. Here's my final working solution (explanation below):

// overrides from ItemTouchHelper
override fun onMove(recyclerView: RecyclerView,
                viewHolder: RecyclerView.ViewHolder,
                target: RecyclerView.ViewHolder): Boolean {
    val fromPos = viewHolder.adapterPosition
    val toPos = target.adapterPosition

    val fromSnapshot = snapshots.first { it.order == fromPos }
    val toSnapshot = snapshots.first { it.order == toPos }

    Logger.d("Habit ${fromSnapshot.name} (${fromSnapshot.order}) to $toPos")
    Logger.d("Habit ${toSnapshot.name} (${toSnapshot.order}) to $fromPos")
    fromSnapshot.order = toPos
    toSnapshot.order = fromPos

    hasDragged = true

    notifyItemMoved(toPos, fromPos)
    return true
}
// overrides from FirebaseRecyclerAdapter
override fun onChildChanged(type: ChangeEventType?, snapshot: DataSnapshot?, newIndex: Int, oldIndex: Int) {
    when (type) {
        ChangeEventType.MOVED -> if (!hasDragged) {
            super.onChildChanged(type, snapshot, newIndex, oldIndex)
        }
        else -> super.onChildChanged(type, snapshot, newIndex, oldIndex)
    }
}

override fun onDataChanged() {
    hasDragged = false
}

I may be doing something wrong, but it seemed that this answer above above code duplicated effort since the FirebaseRecyclerAdapter and the observable snapshots will automagically update on notification. This caused the drag to end abruptly with new values. I was also getting wrong order numbers (probably my fault) by using getSnapshots().get(pos) (snapshots[fromPos] in kotlin). This caused some really weird animations. Also when notifying on the move, I had to reverse the to/from (target/viewHolder) positions. However, please note this does rely on ordering in the query using the order field. And finally, I don't want to let the FirebaseRecyclerAdapter#onChildChanged method to call if it's because of a user drag, that also causes unwanted animation and duplicated effort.

like image 23
prodaea Avatar answered Sep 20 '22 02:09

prodaea