После сортировки списка LazyColumn выполняет перекомпоновку, но сразу после этого снова компонует, меняя порядок сортировки

В моей компоновке 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,
    )
}

Этот код далек от работоспособности, поэтому мы не можем его воспроизвести. Пожалуйста, предоставьте минимально воспроизводимый пример.

Leviathan 29.05.2024 20:28

Также важно показать нам, как вызывается injectListFromDatabase и что передается в качестве его параметра.

Leviathan 29.05.2024 20:51

Для меня практически невозможно отделить работоспособный код. Я могу предоставить только весь код с базой данных. Завтра я постараюсь создать новый общедоступный проект на GitHub.

djtomahou 29.05.2024 21:11

Не делайте этого, вопрос должен сам содержать всю необходимую информацию. Не предоставляйте ссылки на исходный код, размещенный на других сайтах. Вместо этого предоставьте весь соответствующий код напрямую и удалите все ненужное. Если у вас возникли проблемы с этим, создайте новый проект и скопируйте код из вашего вопроса в этот новый проект. Для каждой ошибки компиляции решите, можно ли удалить эту часть, в противном случае добавьте минимальную часть кода, чтобы он просто компилировался. Это не должно иметь смысла, его просто нужно скомпилировать. Затем запустите код и убедитесь, что проблема все еще сохраняется. ...

Leviathan 29.05.2024 21:41

... Затем вы можете вставить этот код в вопрос. Насколько я могу судить, нам не понадобятся файлы Gradle или операторы импорта, а только составной объект, который демонстрирует проблему, и все, что необходимо для этого составного объекта, например модели представлений и классы данных. Помните: не используйте код из текущего проекта, используйте код из нового фиктивного проекта, который содержит только минимальную версию, необходимую для воспроизведения проблемы.

Leviathan 29.05.2024 21:41

Если вам нужны данные для заполнения ваших структур данных, просто придумайте фиктивные данные. Заполните список listOf(...), нет необходимости в каком-либо коде уровня данных, таком как доступ к базе данных, сетевые запросы и т. д.

Leviathan 29.05.2024 21:44

Пожалуйста, создайте минимальный воспроизводимый пример. Это не должно быть долго, достаточно долго, чтобы продемонстрировать проблему. Затем вставьте это в вопрос. Внешние ссылки не подходят для распространения кода, имеющего отношение к вопросу.

Leviathan 30.05.2024 19:00

Я отредактировал свой пост, включив в него полный работоспособный код. Я не в состоянии минимизировать это дальше.

djtomahou 30.05.2024 20:34

Это далеко не минимум. Но, наконец, он предоставляет важную информацию, которую я просил в своих первых двух комментариях. Чтобы сократить ситуацию, я взял на себя смелость и сократил код так, что теперь он стал достаточно минимальным. Это довольно близко к тому, что у вас было изначально, я просто добавил те части, которые были необходимы для воспроизведения проблемы. Однако это то, что вы должны были сделать сами, прежде чем даже публиковать вопрос.

Leviathan 31.05.2024 02:01

Мне потребовалось некоторое время, чтобы понять ваш дамп кода, но StackOverflow предназначен не для этого: он фокусируется на предоставлении решений проблем, поэтому необходим только самый короткий код, создающий проблему. Все остальное только усложняет нам чтение, понимание и ответ на ваш вопрос. Это не в ваших интересах и не в наших. В туре упоминается, что цель StackOverflow — «создать библиотеку подробных высококачественных ответов». Для этого необходимо, чтобы и вопросы были качественными. Пожалуйста, имейте это в виду для будущих вопросов, которые вы, возможно, захотите задать.

Leviathan 31.05.2024 02:02
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
10
63
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Проблема действительно в том, как вы звоните injectListFromDatabase. При каждой (пере)композиции выполняется эта строка кода:

if (listFromDatabase.isNotEmpty()) component.injectListFromDatabase(listFromDatabase)

Это не повредит, пока список пуст, но когда нажата «Поиск», это выполняется не только один раз, но и повторяется при каждой рекомпозиции.

Давайте посмотрим, что это означает, когда после «Поиска» нажата «Сортировка»:

  1. component.sort() вызывается и меняет stateList, так что теперь он отсортирован правильно. Вы уже убедились в этом своими println высказываниями.
  2. Изменение stateList запускает рекомпозицию, поэтому View выполняется снова.
  3. injectListFromDatabase называется. Это заменяет содержимое stateList на listFromDatabase. Это означает, что список, который мы только что отсортировали в 1., никогда не отображается, он просто выбрасывается и вместо него используется listFromDatabase.
  4. При повторном нажатии кнопки «Сортировать» все повторяется с 1., поэтому ничего не изменится.

Проблема в том, что injectListFromDatabase вызывается не только при изменении listFromDatabase, он также вызывается при каждой второй рекомпозиции.

Исправить это просто:

LaunchedEffect(listFromDatabase) {
    if (listFromDatabase.isNotEmpty()) component.injectListFromDatabase(listFromDatabase)
}

Это оборачивает injectListFromDatabase в лямбду, которая выполняется только при изменении параметров LaunchedEffect. Я указал listFromDatabase в качестве параметра, так что это эффективно пропускает выполнение injectListFromDatabase для каждой рекомпозиции, где listFromDatabase не изменилось - как это происходит при нажатии кнопки «Сортировать».

Сортировка теперь работает и после нажатия кнопки "Поиск".

И мне потребовалось 8 часов, чтобы минимизировать мой код, а мог бы быть еще меньше. Спасибо, это всего лишь мой второй проект, поэтому я не очень хорош в написании кода, из которого легко вынимать части, а у меня всего более 6000 строк кода. Еще раз спасибо за вашу помощь, я знал, что это будет что-то тривиальное и связанное с сопрограммами. В течение 7 дней я пытался найти решение, но не помнил о существовании LaunchedEffect.

djtomahou 31.05.2024 09:01

Мне жаль, что это отняло у вас так много времени, но StackOverflow просто не подходит для вопросов, которые четко не определены. Для подобных вопросов невозможно обойтись без минимального воспроизводимого примера. Но я рад, что в конце концов это сработало, и, возможно, это даже поможет вам с будущими вопросами.

Leviathan 31.05.2024 09:36

Нет, я не жалуюсь, что это заняло у меня так много времени, это был хороший опыт, я рад, что моя проблема была решена. Я просто надеюсь, что вам не потребовалось много времени, чтобы уменьшить его еще больше. Я пытался изменить listFromDatabase с mutableStateOf на только список, но до сих пор не понимаю, почему он каждый раз перекомпоновывается. Нужно еще немного изучить Котлин.

djtomahou 31.05.2024 16:47
listFromDatabase никогда не был MutableState, это простой параметр списка составного View. Дополнительная рекомпозиция происходит потому, что injectListFromDatabase меняет MutableState stateList. Без LaunchedEffect это изменение происходит при каждой рекомпозиции (например, при нажатии кнопки сортировки), с LaunchedEffect это происходит только при изменении listFromDatabase. Однако это не связано с Kotlin, именно так работает Compose.
Leviathan 31.05.2024 17:02

То есть, если я правильно понимаю, условие if (listFromDatabase.isNotEmpty()) проверяется при каждой перекомпозиции? А еще я попробовал поменять LaunchedEffect на AtomicBoolean и это тоже работает. Мне просто нужно было убедиться, что первый if в представлении запускается только один раз. Так просто, и я до сих пор не думал об этом более 7 дней.

djtomahou 31.05.2024 22:15

Рекомпозиция всегда включает в себя выполнение всей функции от начала до конца. Когда stateList изменяется, запускается перекомпозиция View, и все, что там находится, выполняется снова. if (listFromDatabase.isNotEmpty()) — это первое, что выполняется (если, конечно, оно не обернуто в LaunchedEffect).

Leviathan 31.05.2024 22:46

Итак, я с самого начала понял это неправильно, я думал, что в функцию перекомпоновываются только те вещи, состояние которых изменилось. Я думал, что если listFromDatabase не изменится, if состояние не сработает. Моя вина с самого начала. Спасибо за разъяснение.

djtomahou 02.06.2024 00:22

Это по-прежнему код Kotlin, вы не можете выполнять только части функции, это не проблема. Особенностью функций Compose является то, что среда выполнения Compose отслеживает активные компонуемые объекты и может вызывать их, когда это будет сочтено необходимым (это называется рекомпозицией). Во время рекомпозиции он также может пропускать вызовы функций Compose, параметры которых не изменились. Но это относится только к функциям Compose. if (listFromDatabase.isNotEmpty()) не является функцией Compose, поэтому она выполняется при каждой рекомпозиции. LaunchedEffect — это функция Compose, поэтому она пропускается, если ее параметр одинаковый.

Leviathan 02.06.2024 00:33

Еще раз спасибо за это объяснение. Теперь я понимаю, как я совершил эту ошибку.

djtomahou 02.06.2024 17:39

Другие вопросы по теме