Home » Android » java – how to use drag and drop on recycler view using firebase realtime database

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

Posted by: admin May 14, 2020 Leave a comment

Questions:

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.

How to&Answers:

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.

Answer:

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")
}

Answer:

I had an issue similar to this and have done the following (assuming you are using a class which extends ItemTouchHelper.SimpleCallback e.g. RecyclerViewTouchHelper):

1) Add a property to your Firebase child to record its order in the list (e.g. orderNumber).

2) Modify the onMove() method of your RecyclerViewTouchHelper to include the following:

@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {

    final int firstPosition = viewHolder.getAdapterPosition();
    final int secondPosition = target.getAdapterPosition();
    DatabaseReference firstItemRef = mMovieAdapter.getRef(viewHolder.getAdapterPosition());
    DatabaseReference secondItemRef = mMovieAdapter.getRef(target.getAdapterPosition());

    HashMap<String, Object> updateFirstItemOrderNumber = new HashMap<>();
    updateFirstItemOrderNumber.put("orderNumber", secondPosition);
    firstItemRef.updateChildren(updateFirstItemOrderNumber);

    HashMap<String, Object> updateSecondItemOrderNumber = new HashMap<>();
    updateSecondItemOrderNumber.put("orderNumber", firstPosition);
    secondItemRef.updateChildren(updateSecondItemOrderNumber);

    return false;
}

3) When creating your RecyclerViewAdapter, make sure that the query you use is ordered by orderNumber e.g.

Query orderedListQuery = FirebaseRef.orderByChild("orderNumber");

Hope this helps!