Вопрос или проблема
У меня есть список элементов, каждый из которых использует Modifier.draganddropsource для передачи ClipData некоторым целям в пределах одного экрана. Когда элемент сбрасывается на целевую область, он удаляется из оригинального списка, и список обновляется. Все выглядит хорошо на экране (новый список отрисовывается без этого элемента). Но когда я пытаюсь перетащить следующий элемент из списка – ниже того, который я только что убрал – его ClipData все еще представляет предыдущий (удаленный) элемент, и целевая область думает, что она снова получает старый элемент.
Компонент Source:
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ItemView(
modifier: Modifier = Modifier,
item: String
) {
val clipState: String by remember(item) {
derivedStateOf { item }
}
// val transferData: suspend DragAndDropSourceScope.() -> Unit by remember(item) {
// Log.d("ItemView", "обновление производного draganddrop для $item")
//
// derivedStateOf {
// {
// detectTapGestures(onPress = {
// Log.d("Source", "начинаем перетаскивать ${item}")
// startTransfer(DragAndDropTransferData(
// ClipData.newPlainText("product", item),
// flags = View.DRAG_FLAG_OPAQUE
// ))
// })
//
// }}
// }
Box(
modifier = modifier
//.dragAndDropSource(block = transferData)
.dragAndDropSource {
detectTapGestures(onPress = {
Log.d("Source", "начинаем перетаскивать ${item}")
startTransfer(DragAndDropTransferData(
ClipData.newPlainText("product", item),
localState = clipState,
flags = View.DRAG_FLAG_OPAQUE
))
})
}
.fillMaxWidth()
.height(48.dp),
contentAlignment = Alignment.CenterStart
) {
Text(
text = item,
color = Color.Black
)
}
}
Обработчик Target:
class Callback(val index: Int): DragAndDropTarget {
...
override fun onDrop(event: DragAndDropEvent): Boolean {
return try {
event.toAndroidDragEvent().localState.also { Log.d("Target", "localState: $it") }
event.toAndroidDragEvent().clipData.getItemAt(0).text.toString()
.also {
Log.d("Target", "перетаскивание $it на $index")
onDrop(index, it)
}
insertIndex = null
true
} catch (e: Exception) {
Log.e("DropCallback", "не удалось разобрать данные", e)
false
}
}
...
}
Как вы можете видеть в комментариях, я действительно пытался сохранить TransferData, а затем весь блок источников в запомнённом состоянии со временем, связанным с элементом, но это не дало эффекта. Я не смог разобраться, как сохранить функцию onPress таким образом.
Элемент действительно меняется, потому что отображаемый на экране компонент показывает правильный текст. Но каждое сообщение журнала в source и target отражает старое состояние этого компонента, ЗА ИСКЛЮЧЕНИЕМ сообщения “обновление производного draganddrop”, которое показывает правильный новый элемент (все остальные оставшиеся элементы обновляются после изменения списка, это просто функция map, а не LazyColumn). Интересно, что все другие элементы в списке работают правильно – только один элемент ниже того места, где список изменился, имеет эту проблему, но она сохраняется так, что по мере перемещения большего количества элементов, большее количество оставшихся элементов имеет неверные данные. В любом случае, я не эксперт в состоянии и ре-композиции, поэтому надеюсь, что вы сможете указать на что-то, что я упустил. Спасибо
Ответ или решение
Ваша проблема связана с тем, как в Jetpack Compose обрабатывается состояние и композиция. При использовании Modifier.dragAndDropSource
, состояние, связанное с объектом, который вы перетаскиваете, должно быть обновлено в соответствии с состоянием вашего списка. Давайте разберем вашу проблему и предложим решение.
Проблема
При удалении элемента из списка, старые значения ClipData (ClipData) могут по-прежнему оставаться в области видимости, и это связано с тем, что перетаскиваемый объект (DragAndDropSource) не обновляется должным образом. Так как состояние перетаскиваемого элемента зависит от сохраненных значений, когда вы пытаетесь сделать перетаскивание следующего элемента списка, старое состояние еще активно.
Решение
-
Используйте
key
для состояний: Убедитесь, что вы правильно используетеkey
для управления состоянием вremember
. Это поможет избежать хранения старых значений. - Обновление состояния при изменении списка: Когда вы удаляете элемент из списка, важно убедиться, что состояние компонента, использующего
dragAndDropSource
, также обновляется.
Ниже приведен исправленный код с учетом вышеизложенного:
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ItemView(
modifier: Modifier = Modifier,
item: String,
onDragStarted: (String) -> Unit
) {
// Используем вспомогательное состояние для clipData
val clipState: String by remember(item) { derivedStateOf { item } }
Box(
modifier = modifier
.dragAndDropSource {
detectTapGestures(onPress = {
Log.d("Source", "starting to drag $clipState")
startTransfer(
DragAndDropTransferData(
ClipData.newPlainText("product", clipState),
localState = clipState,
flags = View.DRAG_FLAG_OPAQUE
)
)
onDragStarted(clipState) // вызвать функцию на старте перетаскивания
})
}
.fillMaxWidth()
.height(48.dp),
contentAlignment = Alignment.CenterStart
) {
Text(
text = item,
color = Color.Black
)
}
}
Изменения
-
Мы теперь используем
clipState
внутриdragAndDropSource
, что гарантирует, что значения обновляются при каждом перетаскивании. onDragStarted
— это дополнительный коллбэк, который может быть использован для выполнения дополнительных действий, связанных с началом перетаскивания. Это помогает отслеживать текущее состояние элемента во время перетаскивания.
Заключение
Для достижения надежной работы функции Drag and Drop важно убедиться, что состояние обновляется в соответствии с изменениями, происходящими в списке. Использование remember
с правильными ключами и обновление состояния при изменении элементов списка должно решить вашу проблему с устаревшими значениями данных при перетаскивании. Если у вас есть дополнительные вопросы, не стесняйтесь их задавать!