JetPack Compose — Почему DragAndDropSource transferData не отражает новое состояние после рекомпозиции?

Вопрос или проблема

У меня есть список элементов, каждый из которых использует 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) не обновляется должным образом. Так как состояние перетаскиваемого элемента зависит от сохраненных значений, когда вы пытаетесь сделать перетаскивание следующего элемента списка, старое состояние еще активно.

Решение

  1. Используйте key для состояний: Убедитесь, что вы правильно используете key для управления состоянием в remember. Это поможет избежать хранения старых значений.

  2. Обновление состояния при изменении списка: Когда вы удаляете элемент из списка, важно убедиться, что состояние компонента, использующего 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 с правильными ключами и обновление состояния при изменении элементов списка должно решить вашу проблему с устаревшими значениями данных при перетаскивании. Если у вас есть дополнительные вопросы, не стесняйтесь их задавать!

Оцените материал
Добавить комментарий

Капча загружается...