В моей компоновке View у меня есть список элементов в LazyColumn и кнопка «Сортировать», которая переключает порядок сортировки по возрастанию и убыванию. Список взят из моего класса держателей штата ViewComponent. Это работает нормально.
У меня также есть кнопка «Поиск», которая извлекает данные из базы данных, которые затем передаются в View для замены отображаемых данных.
Хотя данные заменены, порядок сортировки теперь нарушен: всякий раз, когда я нажимаю кнопку «Сортировать», список сортируется, а затем мгновенно возвращается обратно. Я проверил с помощью println, что сортировка на самом деле выполняется так, как предполагалось, но как только она завершается, кажется, происходит еще одна рекомпозиция, которая сбрасывает сортировку.
Возможно ли, что за это отвечает функция injectListFromDatabase? Он только заменяет класс данных значениями из базы данных.
Для ясности я сократил количество классов данных до минимума, но на самом деле у них гораздо больше свойств. LazyColumn отображает список объектов GetToolsWithService:
data class GetToolsWithService(
val id: Int,
)
data class ToolListState(
val tools: List<GetToolsWithService>,
)
Это мой View с LazyColumn:
@Composable
fun View(
component: ViewComponent = ViewComponent(),
listFromDatabase: List<GetToolsWithService>,
) {
if (listFromDatabase.isNotEmpty()) component.injectListFromDatabase(listFromDatabase)
val stateList by component.stateList
Button(onClick = {
component.sort()
}) {
Text("Sort")
}
LazyColumn {
items(stateList.tools) { Text("$it") }
}
}
Государствовладелец:
class ViewComponent(
private val sortTable: SortTable = SortTable(),
initialListOfTools: List<GetToolsWithService> = List(10) { GetToolsWithService(it) },
) {
val stateList: MutableState<ToolListState> = mutableStateOf(ToolListState(initialListOfTools))
fun sort() {
println("Sort Start")
stateList.value = stateList.value.copy(tools = sortTable(stateList.value.tools))
stateList.value.tools.forEach { println(it) }
println("Sort end")
}
fun injectListFromDatabase(list: List<GetToolsWithService>) {
stateList.value = stateList.value.copy(tools = list)
}
}
И это используется для сортировки:
enum class SortOrder {
ASCENDING, DESCENDING
}
class SortTable(
private var sortingOrderData: SortOrder = SortOrder.ASCENDING,
) {
operator fun invoke(list: List<GetToolsWithService>): List<GetToolsWithService> =
if (sortingOrderData == SortOrder.DESCENDING) {
sortingOrderData = SortOrder.ASCENDING
list.sortedBy { it.id }
} else {
sortingOrderData = SortOrder.DESCENDING
list.sortedByDescending { it.id }
}
}
Следующее используется только для эмуляции того, как данные из базы данных передаются в View:
Column {
var list: List<GetToolsWithService> by remember { mutableStateOf(emptyList()) }
Button(onClick = {
list = List(10) { GetToolsWithService(it + 100) } // "+100" so the items can be more easily distinguished
}) { Text("Search") }
View(
listFromDatabase = list,
)
}
Также важно показать нам, как вызывается injectListFromDatabase и что передается в качестве его параметра.
Для меня практически невозможно отделить работоспособный код. Я могу предоставить только весь код с базой данных. Завтра я постараюсь создать новый общедоступный проект на GitHub.
Не делайте этого, вопрос должен сам содержать всю необходимую информацию. Не предоставляйте ссылки на исходный код, размещенный на других сайтах. Вместо этого предоставьте весь соответствующий код напрямую и удалите все ненужное. Если у вас возникли проблемы с этим, создайте новый проект и скопируйте код из вашего вопроса в этот новый проект. Для каждой ошибки компиляции решите, можно ли удалить эту часть, в противном случае добавьте минимальную часть кода, чтобы он просто компилировался. Это не должно иметь смысла, его просто нужно скомпилировать. Затем запустите код и убедитесь, что проблема все еще сохраняется. ...
... Затем вы можете вставить этот код в вопрос. Насколько я могу судить, нам не понадобятся файлы Gradle или операторы импорта, а только составной объект, который демонстрирует проблему, и все, что необходимо для этого составного объекта, например модели представлений и классы данных. Помните: не используйте код из текущего проекта, используйте код из нового фиктивного проекта, который содержит только минимальную версию, необходимую для воспроизведения проблемы.
Если вам нужны данные для заполнения ваших структур данных, просто придумайте фиктивные данные. Заполните список listOf(...), нет необходимости в каком-либо коде уровня данных, таком как доступ к базе данных, сетевые запросы и т. д.
Пожалуйста, создайте минимальный воспроизводимый пример. Это не должно быть долго, достаточно долго, чтобы продемонстрировать проблему. Затем вставьте это в вопрос. Внешние ссылки не подходят для распространения кода, имеющего отношение к вопросу.
Я отредактировал свой пост, включив в него полный работоспособный код. Я не в состоянии минимизировать это дальше.
Это далеко не минимум. Но, наконец, он предоставляет важную информацию, которую я просил в своих первых двух комментариях. Чтобы сократить ситуацию, я взял на себя смелость и сократил код так, что теперь он стал достаточно минимальным. Это довольно близко к тому, что у вас было изначально, я просто добавил те части, которые были необходимы для воспроизведения проблемы. Однако это то, что вы должны были сделать сами, прежде чем даже публиковать вопрос.
Мне потребовалось некоторое время, чтобы понять ваш дамп кода, но StackOverflow предназначен не для этого: он фокусируется на предоставлении решений проблем, поэтому необходим только самый короткий код, создающий проблему. Все остальное только усложняет нам чтение, понимание и ответ на ваш вопрос. Это не в ваших интересах и не в наших. В туре упоминается, что цель StackOverflow — «создать библиотеку подробных высококачественных ответов». Для этого необходимо, чтобы и вопросы были качественными. Пожалуйста, имейте это в виду для будущих вопросов, которые вы, возможно, захотите задать.





Проблема действительно в том, как вы звоните injectListFromDatabase. При каждой (пере)композиции выполняется эта строка кода:
if (listFromDatabase.isNotEmpty()) component.injectListFromDatabase(listFromDatabase)
Это не повредит, пока список пуст, но когда нажата «Поиск», это выполняется не только один раз, но и повторяется при каждой рекомпозиции.
Давайте посмотрим, что это означает, когда после «Поиска» нажата «Сортировка»:
component.sort() вызывается и меняет stateList, так что теперь он отсортирован правильно. Вы уже убедились в этом своими println высказываниями.stateList запускает рекомпозицию, поэтому View выполняется снова.injectListFromDatabase называется. Это заменяет содержимое stateList на listFromDatabase. Это означает, что список, который мы только что отсортировали в 1., никогда не отображается, он просто выбрасывается и вместо него используется listFromDatabase.Проблема в том, что injectListFromDatabase вызывается не только при изменении listFromDatabase, он также вызывается при каждой второй рекомпозиции.
Исправить это просто:
LaunchedEffect(listFromDatabase) {
if (listFromDatabase.isNotEmpty()) component.injectListFromDatabase(listFromDatabase)
}
Это оборачивает injectListFromDatabase в лямбду, которая выполняется только при изменении параметров LaunchedEffect. Я указал listFromDatabase в качестве параметра, так что это эффективно пропускает выполнение injectListFromDatabase для каждой рекомпозиции, где listFromDatabase не изменилось - как это происходит при нажатии кнопки «Сортировать».
Сортировка теперь работает и после нажатия кнопки "Поиск".
И мне потребовалось 8 часов, чтобы минимизировать мой код, а мог бы быть еще меньше. Спасибо, это всего лишь мой второй проект, поэтому я не очень хорош в написании кода, из которого легко вынимать части, а у меня всего более 6000 строк кода. Еще раз спасибо за вашу помощь, я знал, что это будет что-то тривиальное и связанное с сопрограммами. В течение 7 дней я пытался найти решение, но не помнил о существовании LaunchedEffect.
Мне жаль, что это отняло у вас так много времени, но StackOverflow просто не подходит для вопросов, которые четко не определены. Для подобных вопросов невозможно обойтись без минимального воспроизводимого примера. Но я рад, что в конце концов это сработало, и, возможно, это даже поможет вам с будущими вопросами.
Нет, я не жалуюсь, что это заняло у меня так много времени, это был хороший опыт, я рад, что моя проблема была решена. Я просто надеюсь, что вам не потребовалось много времени, чтобы уменьшить его еще больше. Я пытался изменить listFromDatabase с mutableStateOf на только список, но до сих пор не понимаю, почему он каждый раз перекомпоновывается. Нужно еще немного изучить Котлин.
listFromDatabase никогда не был MutableState, это простой параметр списка составного View. Дополнительная рекомпозиция происходит потому, что injectListFromDatabase меняет MutableState stateList. Без LaunchedEffect это изменение происходит при каждой рекомпозиции (например, при нажатии кнопки сортировки), с LaunchedEffect это происходит только при изменении listFromDatabase. Однако это не связано с Kotlin, именно так работает Compose.
То есть, если я правильно понимаю, условие if (listFromDatabase.isNotEmpty()) проверяется при каждой перекомпозиции? А еще я попробовал поменять LaunchedEffect на AtomicBoolean и это тоже работает. Мне просто нужно было убедиться, что первый if в представлении запускается только один раз. Так просто, и я до сих пор не думал об этом более 7 дней.
Рекомпозиция всегда включает в себя выполнение всей функции от начала до конца. Когда stateList изменяется, запускается перекомпозиция View, и все, что там находится, выполняется снова. if (listFromDatabase.isNotEmpty()) — это первое, что выполняется (если, конечно, оно не обернуто в LaunchedEffect).
Итак, я с самого начала понял это неправильно, я думал, что в функцию перекомпоновываются только те вещи, состояние которых изменилось. Я думал, что если listFromDatabase не изменится, if состояние не сработает. Моя вина с самого начала. Спасибо за разъяснение.
Это по-прежнему код Kotlin, вы не можете выполнять только части функции, это не проблема. Особенностью функций Compose является то, что среда выполнения Compose отслеживает активные компонуемые объекты и может вызывать их, когда это будет сочтено необходимым (это называется рекомпозицией). Во время рекомпозиции он также может пропускать вызовы функций Compose, параметры которых не изменились. Но это относится только к функциям Compose. if (listFromDatabase.isNotEmpty()) не является функцией Compose, поэтому она выполняется при каждой рекомпозиции. LaunchedEffect — это функция Compose, поэтому она пропускается, если ее параметр одинаковый.
Еще раз спасибо за это объяснение. Теперь я понимаю, как я совершил эту ошибку.
Этот код далек от работоспособности, поэтому мы не можем его воспроизвести. Пожалуйста, предоставьте минимально воспроизводимый пример.