Почему моя переменная размера не обновляется правильно в Jetpack Compose?

Я изучаю Jetpack Compose и создал простой пример, чтобы понять управление состоянием. У меня есть кнопка, которая обновляет переменную width, и я хочу вычислить size на основе переменных width и height. Однако переменная size не обновляется должным образом при изменении width.

Вот упрощенный код, демонстрирующий проблему:

Пример 1 (не работает):

@Composable
@Preview
fun App() {
    var width by remember { mutableStateOf(0f) }
    var height by remember { mutableStateOf(0f) }
    
    // Issue in this line. When width or height changes, size is not recalculated.
    // size remains the same as initial state Size(0, 0).
    // I expect that the size to be always sync with the width and height
    var size = Size(width, height)

    Button(
        onClick = { width++ },
        modifier = Modifier.pointerInput(Unit) {
            detectDragGestures { change, dragAmount ->
                println(size)
                println(width)
            }
        }
    ) {
        Text("OK")
    }
}

Когда я нажимаю кнопку, width увеличивается правильно, но size всегда печатается Size(0.0, 0.0), когда я перетаскиваю кнопку. Я пробовал использовать mutableStateOf вместо size, но все равно не работает.

Пример 2 (использование производногоStateOf, работает):

@Composable
@Preview
fun App() {
    var width by remember { mutableStateOf(0f) }
    var height by remember { mutableStateOf(0f) }

    val size by derivedStateOf { Size(width, height) }

    Button(
        onClick = { width++ },
        modifier = Modifier.pointerInput(Unit) {
            detectDragGestures { change, dragAmount ->
                println(size)
                println(width)
            }
        }
    ) {
        Text("OK")
    }
}

Единственное работающее решение — это использование derivedStateOf, но в документации указано, что derivedStateOf следует использовать, когда входные данные меняются чаще, чем это необходимо для рекомпозиции, а в данном случае это не так.

Вам следует использовать функцию derivedStateOf, когда вы вводите данные в Composable меняются чаще, чем вам нужно перекомпоновать. Этот часто происходит, когда что-то часто меняется, например, прокрутка положение, но компонуемому нужно реагировать на него только после того, как оно пересечет определенный порог. derivedStateOf создает новое состояние Compose объект, вы можете заметить, что он обновляется только столько, сколько вам нужно. В этом Кстати, он действует аналогично Kotlin Flows distinctUntilChanged().

Внимание: derivedStateOf стоит дорого, и его следует использовать только во избежание ненужной рекомпозиции, если результат не изменился.

Пример 3 (Кажется, работает, но почему?):

@Composable
@Preview
fun App() {
    var width by remember { mutableStateOf(0f) }
    var height by remember { mutableStateOf(0f) }

    val size by mutableStateOf(Size(width, height))

    println(width)
    println(size)

    Button(
        onClick = { width++ }
    ) {
        Text("OK")
    }
}

В этом примере size выглядит корректно, но я не понимаю, почему.

Вопрос: Я понимаю, что derivedStateOf часто используется для оптимизации производительности, но я пытаюсь понять, почему size не обновляется правильно с помощью mutableStateOf или как обычная переменная в первом примере. Кроме того, почему третий пример работает?

2
0
164
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Попробуйте либо:

val size by remember { derivedStateOf { Size(width, height) } }

или:

val size = remember(width, height) { Size(width, height) } // don't use this in the lambda
val currentSize by rememberUpdatedState(size) // use this in the lambda

Спасибо за Ваш ответ! Я понимаю, что derivedStateOf работает, но мне интересно, почему это работает в данном случае. В документации также предполагается, что derivedStateOf может не подойти для этой ситуации. Второй подход с mutableStateOf не работает независимо от того, используются ли клавиши с запоминанием или нет. Однако использование rememberUpdatedState работает. Мой вопрос: почему происходит такое поведение, помимо простого предоставления правильного решения?

Abdo21 20.07.2024 18:15

Подождите, 2-й подход даже с ключами не работает? Это очень странно. Я думаю, так и должно быть. О_о, ну, если нет, я просто отразлю детали реализации rememberUpdatedState в редактировании.

EpicPandaForce 20.07.2024 22:34

Второй подход с ключами не работает, потому что при изменении ключей вы создаете новый экземпляр State, но лямбда-выражение pointerInput по-прежнему работает с исходным экземпляром. Это также верно и для отредактированного ответа, поэтому я предполагаю, что это тоже не сработает. Вам нужно сохранить исходный экземпляр State и изменить только значение (именно это и делает RememberUpdatedState).

Jan Bína 20.07.2024 23:31

@JanBína, ты абсолютно прав, он все равно фиксирует текущую лямбду. Ох, теперь я знаю, что делать, спасибо!

EpicPandaForce 22.07.2024 15:58

Отредактировал, чтобы исправить.

EpicPandaForce 23.07.2024 22:05

Документация pointerInput объясняет это:

Когда модификатор pointerInput создается композицией, если block захватывает любой локальный переменных, с которыми нужно работать, для работы с изменениями этих переменных обычно используются два шаблона. в зависимости от желаемого поведения.

Указание захваченного значения в качестве параметра key приведет к отмене block и перезапуску с самого начала, если значение изменится: keyedPointerInputModifier

If block не должен перезапускаться при изменении захваченного значения, но значение все равно должно обновиться для следующего использования, используйте RememberUpdatedState для обновления держателя значения, к которому обращается block: RememberUpdatedParameterPointerInputModifier

Для вашего варианта использования я бы порекомендовал rememberUpdatedState:

val size = rememberUpdatedState(Size(width, height))

В первом примере вы создаете новый экземпляр Size в каждой композиции, но блок pointerInput не создается заново и все еще ссылается на старый экземпляр. Аналогично, в третьем примере вы создаете новый экземпляр State, потому что вы не обертываете его remember. rememberUpdatedState эффективно делает это:

remember {
    mutableStateOf(newValue)
}.apply { value = newValue }

Таким образом, вы сохраняете тот же экземпляр State, и когда вы вызываете State.value внутри лямбды, вы получаете новое значение.

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

Это немного сложно. В дополнение к остальным ответам я постараюсь более подробно объяснить, что именно происходит в ваших трех примерах.

В первом примере вы передаете лямбду в pointerInput (часть в фигурных скобках). Это означает, что вы передаете только функцию: код в лямбде на самом деле не выполняется сразу, вы просто предоставляете функцию, которая может быть выполнена позже. Поскольку вы получаете доступ к переменным в вашей лямбде, которые находятся за пределами лямбды, Котлин фиксирует значения переменных в замыкании . Это просто означает, что значения где-то хранятся, поэтому к ним можно будет получить доступ позже, когда лямбда-выражение действительно будет выполнено.

Двумя переменными, к которым обращается лямбда, являются pointerInput и size. В момент первого вызова widthpointerInput является size, так что объект захватывается и для Size(0f, 0f) (по крайней мере, так кажется, читайте дальше, чтобы узнать больше) width захватывается. Поэтому всякий раз, когда выполняется лямбда, осуществляется доступ к этим двум значениям. Поэтому вы видите только 0f.

Теперь более интересный вопрос: почему вы видите все изменения в Size(0f, 0f), поскольку он фиксируется так же, как width. Разница в том, что size на самом деле является делегированным значением , потому что вы объявили его с помощью width. Это всего лишь синтаксический сахар в Котлине, поэтому в вашем коде вы можете получить доступ к by как к width, где фактический объект — это Float. Во время компиляции Kotlin заменяет вашу переменную MutableState<Float>Float на width и везде, где вы обращаетесь к переменной, он фактически вызывает MutableState<Float>.

Это означает, что в вашей лямбде width.value вы фактически получаете доступ к объекту типа pointerInput (а затем вызываете MutableState<Float>) при доступе к value. И в этом разница с width: лямбда фиксирует этот size объект, но любые изменения в MutableState<Float> на самом деле только обновляют внутреннее свойство width этого самого объекта, но никогда не создают новый объект. В отличие от value, где каждый раз создается новый size объект. Вот почему Size(...) работает, а width нет, так что то, что size вообще работает, скорее совпадение.

Когда width изменяется, запускается рекомпозиция, которая фактически выполняется width снова, поэтому можно подумать, что все вышеперечисленное не имеет значения, потому что каждый раз, когда новая лямбда передается pointerInput с текущими на тот момент объектами, но pointerInput только обновляет лямбду (и перезапускает его) при изменении ключей, которые вы предоставляете в качестве параметров. И поскольку вы указали pointerInput для этого ключа, pointerInput всегда сохраняет старую лямбду, созданную при первом вызове pointerInput. Более подробно это объясняется в документации pointerInput.

Вы можете просто исправить свой код, если укажете Unit в качестве ключа (вместо size), тогда при каждом изменении значений в лямбда-выражении снова фиксируются, и все работает как задумано.

Теперь перейдем ко второму примеру, который, к счастью, намного проще: поскольку вы также используете Unit с делегированием derivedStateOf, у вас под капотом фактически есть объект State, например by, который фиксируется в лямбде. Это работает по тем же причинам, по которым width работал в первом примере.

За исключением одного предостережения: поскольку вы не сделали width производное состояние, при каждой рекомпозиции создается и сохраняется новое состояние remember. Однако это не имеет значения, поскольку лямбда использует только первый объект состояния, который был создан таким образом, и который автоматически обновляется при изменении size (или width, если уж на то пошло), вот как height работает. Но вам также следует улучшить это состояние, чтобы оно не воссоздавалось без необходимости при перекомпоновке.

А в третьем примере вам даже не нужно изменяемое состояние, поскольку операторы печати не помещаются внутри ламбы:

val size = Size(width, height)

Это также просто работает так, как ожидалось.

Спасибо за подробный ответ! У меня еще есть один вопрос. Если в первом примере я использую: val size by remember{mutableStateOf(Size(width,height))}. это должно работать аналогично тому, почему width работает, как вы объяснили, но это не работает! Почему это? Даже если я добавлю width и height в качестве ключей к remember, это все равно не сработает. Почему?

Abdo21 20.07.2024 18:06

Это будет выполнено только один раз, в первой композиции. Он не будет автоматически создавать новые Size объекты при каждой рекомпозиции. Чтобы принудительно это сделать, вам необходимо предоставить width и height в качестве ключей, чтобы они пересчитывались при их изменении: val size by remember(width, height) { mutableStateOf(Size(width, height)) }. И теперь это сработает (по той же причине width уже сработало).

Leviathan 20.07.2024 18:19

Не работает даже при использовании ключей! В отладчике я вижу, что значение размера обновляется правильно, но лямбда-выражение по-прежнему печатает ноль.

Abdo21 20.07.2024 18:30

Ах, извините, вы, конечно, правы: хотя сейчас это правильно обновляет size (чего раньше не было), каждый раз создается новый объект State. Так что это больше похоже на то, что было в вашем первом примере вначале. Это помогает только предотвратить воссоздание объекта Size(...) при рекомпозиции, где width и height остаются прежними (т. е. рекомпозиция была вызвана чем-то другим). Вам все равно нужно передать size в качестве параметра key в pointerInput.

Leviathan 20.07.2024 18:37

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