How to create LazyColumn with drag and drop elements in Jetpack Compose. Part 1.
Tags: android, compose
Published: 2024-06-23 21:45:47

Hello there! The subject of drag and drop elements isn't new and you can find many decisions of this problem in the Internet. But the decision isn't so obviously like a creating drag and drop lists by RecyclerView. And I suppose my step-by-step guide will be useful.

#### Sources:

- [Make it Easy: How to implement Drag and Drop List Item in Jetpack Compose](https://www.youtube.com/watch?v=jVL6Ze46III)
- [LazyColumnDragAndDropDemo.kt](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt)

If you just want to solve your problem you can get code from [LazyColumnDragAndDropDemo.kt](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt) and implement it to your project.

So, I'm getting a solution from [Make it Easy: How to implement Drag and Drop List Item in Jetpack Compose](https://www.youtube.com/watch?v=jVL6Ze46III). I'll try to explain step-by-step what happens. Next, I'll solve issues and do refactoring. At the end of refactoring I wanna get something like [LazyColumnDragAndDropDemo.kt](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt).

_At the beginning I wanna say that the most interesting will be in next parts. You can think of this part as a preview._

OK, let's get started.

### About start commit of app
[Starter code](https://github.com/MarDSoul/example-android-draganddroplazycolumn/tree/69375902142800e650e4ae443446c4a90735a332)
So, we have three layers in our app:
- **UI** - consists of one screen with two buttons (Add user and Clear list) and `class AppViewModel` and `data class UserEntityUi`
- **Domain** - consists of `data class UserEntity` and `interface UserRepository`
- **Data** - consists of database (created by Room) and `class UserRepositoryImpl`

Also we have Hilt for DI.

ViewModel is subscribed on changes of `Flow<List<UserEntity>>` and the screen `collectAsState()` this `StateFlow`. _nothing much..._

### Step 1
First of all we need to add a reaction on gestures
```
LazyColumn(
	modifier = Modifier
		.fillMaxSize()
		.weight(1f)
		.pointerInput(Unit) {
			detectDragGesturesAfterLongPress(
				onDrag = { },
				onDragStart = { },
				onDragEnd = { },
				onDragCancel = { }
			)
		}
)
```
And reaction we add by [Modifier.pointerInput](https://developer.android.com/reference/kotlin/androidx/compose/ui/input/pointer/package-summary#(androidx.compose.ui.Modifier).pointerInput(kotlin.Any,kotlin.coroutines.SuspendFunction1)) and [detectDragGesturesAfterLongPress](https://developer.android.com/reference/kotlin/androidx/compose/foundation/gestures/package-summary#(androidx.compose.ui.input.pointer.PointerInputScope).detectDragGesturesAfterLongPress(kotlin.Function1,kotlin.Function0,kotlin.Function0,kotlin.Function2)). You can read about an understanding gestures in [Understand gestures | Google Developers](https://developer.android.com/develop/ui/compose/touch-input/pointer-input/understand-gestures) by yourself. I just don't want to copy text of official guides and documentation it's not correctly.

### Step 2
As usual in Compose we need some state for drag and drop item. Definitely inside this state we need to have [LazyListState](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyListState) and some action for moving element `onMove: (Int, Int) -> Unit`.
OK, let's create it.
```
class DragAndDropListState(
	val lazyListState: LazyListState,
	private val onMove: (Int, Int) -> Unit
)
```
And of course we need to remember our state.
```
@Composable
fun rememberDragAndDropListState(
	lazyListState: LazyListState,
	onMove: (Int, Int) -> Unit
): DragAndDropListState {
	return remember { DragAndDropListState(lazyListState, onMove) }
}
```
And let's create some extension for moving in a `mutableList`
```
fun <T> MutableList<T>.move(from: Int, to: Int) {
	if (from == to) return
	val element = this.removeAt(from)
	this.add(to, element)
}
```
Next let's inject them into our screen
```
is UiState.Success -> {
	val users = state.data.toMutableStateList()
	val dragAndDropListState =
		rememberDragAndDropListState(lazyListState) { from, to ->
			users.move(from, to)
		}
/* LazyColumn etc. */
}
```
`toMutableStateList()` function create `SnapshotStateList<T>` that can be observed and snapshot.

### Step 3
So, we see 4 params in `detectDragGesturesAfterLongPress` which we should implement: `onDrag`, `onDragStart`, `pnDragEnd`, `onDragCancel`. Go back to our `class DragAndDropListState` and start...

#### onStart
The first of all we need to know which element we dragged. It needs some variables:
```
private var initialDraggingElement by mutableStateOf<LazyListItemInfo?>(null)
var currentIndexOfDraggedItem by mutableStateOf<Int?>(null)
```
And now we can implement `onDragStart` function for setting initial values of current element.
But first let's create an extension to getting offset of end of element:
```
private val LazyListItemInfo.offsetEnd: Int
	get() = this.offset + this.size
```
```
fun onDragStart(offset: Offset) {
	lazyListState.layoutInfo.visibleItemsInfo
		.firstOrNull { item -> offset.y.toInt() in item.offset..item.offsetEnd }
		?.also {
			initialDraggingElement = it
			currentIndexOfDraggedItem = it.index
		}
}
```
So, let's paste `onStart()` into `LazyColumn`
```
LazyColumn(
	modifier = Modifier
		.fillMaxSize()
		.weight(1f)
		.pointerInput(Unit) {
			detectDragGesturesAfterLongPress(
				onDrag = { },
				onDragStart = { offset ->
					dragAndDropListState.onDragStart(offset)
				},
				onDragEnd = { },
				onDragCancel = { }
			)
		}
){
/*Composable content*/		
}
```

#### onDrag
What do we wanna do?
- we want to move an element along the Y coordinate
- when we moved an element to the place of another element it needs to change indexes of both elements

OK, let's create a variable for distance of offset
```
private var draggingDistance by mutableFloatStateOf(0f)
fun onDrag(offset: Offset) {
	draggingDistance += offset.y
}
```
Next we need to get offsets of our initial element
```
private var draggingDistance by mutableFloatStateOf(0f)
private val initialOffsets: Pair<Int, Int>?
	get() = initialDraggingElement?.let { Pair(it.offset, it.offsetEnd) }

fun onDrag(offset: Offset) {
	draggingDistance += offset.y

	initialOffsets?.let { (top, bottom) ->
		val startOffset = top.toFloat() + draggingDistance
		val endOffset = bottom.toFloat() + draggingDistance
	}
}
```
Next I gonna directly get the element. In LazyList (LazyColumn or LazyRow) index of element isn't equal a position on a screen. So, we need some extension for getting the element by index.
```
private fun LazyListState.getVisibleItemInfo(itemPosition: Int): LazyListItemInfo? {
	return this.layoutInfo.visibleItemsInfo.getOrNull(itemPosition - this.firstVisibleItemIndex)
	}
```
```
private val currentElement: LazyListItemInfo?
	get() = currentIndexOfDraggedItem?.let {
		lazyListState.getVisibleItemInfo(it)
	}
```
Next our task is changing indexes - we have to change the index of element to which we moved the dragged element, and we have to change index of dragged element.
```
fun onDrag(offset: Offset) {
	draggingDistance += offset.y

	initialOffsets?.let { (top, bottom) ->
		val startOffset = top.toFloat() + draggingDistance
		val endOffset = bottom.toFloat() + draggingDistance

		currentElement?.let { current ->
			lazyListState.layoutInfo.visibleItemsInfo
				.filterNot { item ->
					item.offsetEnd < startOffset || item.offset > endOffset || current.index == item.index
				}
				.firstOrNull { item ->
					val delta = startOffset - current.offset
					when {
						delta < 0 -> item.offset > startOffset
						else -> item.offsetEnd < endOffset
					}
				}
		}?.also { item ->
			currentIndexOfDraggedItem?.let { current ->
				onMove.invoke(current, item.index)
			}
			currentIndexOfDraggedItem = item.index
		}
	}
}
```
`onDrag` function is completed. Let's paste it to `LazyColumn`.
```
detectDragGesturesAfterLongPress(
	onDrag = { change, offset ->
		change.consume()
		dragAndDropListState.onDrag(offset)

/*some code below*/
```
But it's not all because if we try to use it we can see that list doesn't scroll behind dragged item. For scrolling list we have to add over scroll checker and some CoroutineJob for the `.scrollBy()` function.

Add over scroll checker function to `class DragAndDropListState`
```
fun checkOverscroll(): Float {
	return initialDraggingElement?.let {
		val startOffset = it.offset + draggingDistance
		val endOffset = it.offsetEnd + draggingDistance

		return@let when {
			draggingDistance > 0 -> {
				(endOffset - lazyListState.layoutInfo.viewportEndOffset).takeIf { diff -> diff > 0 }

			}

			draggingDistance < 0 -> {
				(startOffset - lazyListState.layoutInfo.viewportStartOffset).takeIf { diff -> diff < 0 }
			}

			else -> null
		}
	} ?: 0f
}
```

Add Coroutine Scope and Coroutine Job to the top of list
```
val coroutineScope = rememberCoroutineScope()
var overscrollJob by remember { mutableStateOf<Job?>(null) }
```

And add `scrollBy()` to onDrag
```
onDrag = { change, offset ->
	change.consume()
	dragAndDropListState.onDrag(offset)

	if (overscrollJob?.isActive == true) return@detectDragGesturesAfterLongPress

	dragAndDropListState
		.checkOverscroll()
		.takeIf { it != 0f }
		?.let {
			overscrollJob = coroutineScope.launch {
				dragAndDropListState.lazyListState.scrollBy(it)
			}
		} ?: kotlin.run { overscrollJob?.cancel() }

}
```

#### onDragEnd, onDragCancel
Here we just reset variables in `class DragAndDropListState`
```
fun onDragInterrupted() {
	initialDraggingElement = null
	currentIndexOfDraggedItem = null
	draggingDistance = 0f
}
```
### Step 4
There is one thing what we have to do. We should define the modifier of element.

Let's create a variable inside `class DragAndDropListState`
```
val elementDisplacement: Float?
	get() = currentIndexOfDraggedItem?.let {
		lazyListState.getVisibleItemInfo(it)
	}?.let { itemInfo ->
		(initialDraggingElement?.offset ?: 0f).toFloat() + draggingDistance - itemInfo.offset
	}
```

And our element's modifier looks like this:
```
ItemCard(
	userEntityUi = user,
	modifier = Modifier
		.composed {
			val offsetOrNull =
				dragAndDropListState.elementDisplacement.takeIf {
					index == dragAndDropListState.currentIndexOfDraggedItem
				}
			Modifier.graphicsLayer {
				translationY = offsetOrNull ?: 0f
			}
		}
)
```
### Preliminary results
Now we have the same code like in [Make it Easy: How to implement Drag and Drop List Item in Jetpack Compose](https://www.youtube.com/watch?v=jVL6Ze46III) video. 

You can get the code by this link: [Part 1 code](https://github.com/MarDSoul/example-android-draganddroplazycolumn/tree/99c493a5beaeac77d49e3ac79044667fbe94457f)

And this even works :)
![Working example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t9hpnx050qsjqyvvqt8l.gif)
#### Issues
- If we add a new user our list back to the starter order of elements. It happens we add user to a database, from the database we receive a new users list, obviously our `uiState` also have a new value, next recomposition.
- If we add a new user and try to interact with last elements we can catch **Index Of Bounds Exception**. It happens because we have no any keys for update `DragAndDropListState`.

---
_Refactoring and resolving issues will be in next Part 2 (place for link)._