Тупая перекомпозиция в Android Compose

#android #android-layout #android-jetpack-compose

#Android #android-макет #android-реактивный ранец-compose

Вопрос:

Рассмотрим этот минимальный фрагмент кода (в Kotlin):

 import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import java.time.LocalDateTime
import java.util.*

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {

            var time by remember {
                mutableStateOf("time")
            }
            Column(modifier = Modifier.clickable { time = LocalDateTime.now().toString() }) {
                Text(text = UUID.randomUUID().toString())
                Text(text = time)
            }
        }
    }
}
 

Рассматривая приведенный выше код с логической точки зрения, можно ожидать, что при нажатии на Column , поскольку time изменяется только параметр Text , будет перерисовываться только составное значение с меньшим временем. Это потому, что перекомпозиция пропускает как можно больше.

Однако обнаруживается, что верхний Text составной элемент также перерисовывается (отображаемый UUID продолжает меняться).

  1. Почему это так?

Обратите внимание, что неидемпотентность моего Column составного элемента не должна иметь никакого отношения, если перерисовка не является тупой.

Ответ №1:

Вы можете попробовать запустить этот блок кода

 @Composable
fun IdempotenceTest() {    
    var time by remember {
        mutableStateOf("time")
    }
    Column(
        modifier = Modifier.clickable {
            time = LocalDateTime.now().toString()
        }
    ) {
        Text(text = getRandomUuid())
        TestComposable(text = returnSameValue())
        Text(text = time)
    }
}

@Composable
fun TestComposable(text: String) {
    SideEffect {
        Log.d(TAG, "TestComposable composed with: $text")
    }
    Text(text = text)
}

private fun getRandomUuid(): String {
    Log.d(TAG, "getRandomUuid: called")
    return UUID.randomUUID().toString()
}

private fun returnSameValue(): String {
    Log.d(TAG, "returnSameValue: called")
    return "test"
}
 

Если вы проверите журналы, вы увидите, что каждый раз, когда изменяется состояние, компилятор пытается повторно вызвать наименьшую заключающую lamda / функцию, где считывается значение состояния. Следовательно, функция IdempotenceTest (в моем примере и setContent{} lamda в вашем) будет повторно выполнена, которая вызовет getRandomUuid функцию returnSameValue and и на основе возвращаемого ими значения решит, следует ли повторно составлять элементы, которые зависят от этих возвращаемых значений. Если вы хотите, чтобы вычисления не повторялись снова и снова, оберните их в remember{} блок.

Теперь, если бы вы использовали Button , например, вместо столбца, вы бы увидели, что была бы выполнена только одна и та же строка содержимого. Причина, по которой это происходит, заключается в том, что Column является встроенной функцией, в то время как Button сама по себе использует Surface внутри нее, которая не является встроенной. Поэтому содержимое Column{} копируется внутри окружающего функционального блока, что приводит к тому, что все IdempotenceTest становится недействительным для перекомпозиции.

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

Чтобы узнать больше о области перекомпозиции, вы можете обратиться к сообщениям в блогах здесь и здесь .

Комментарии:

1. «каждый раз, когда столбец перекомпонируется» .. в этом суть вопроса… почему весь столбец перекомпонируется в первую очередь?

2. «компилятор вызовет getRandomUuid …» почему? разве компилятор не должен вызывать только наблюдение за кодом time ?

3. Я отредактировал ответ, чтобы он содержал больше информации о областях перекомпозиции.

4. этот пост Зака действительно проясняет ситуацию .. спасибо

Ответ №2:

rememberSavable {...} сработало для меня.

Это позволяет вам обновлять состояние непосредственно из clickable и запускать перекомпозицию:

 ...
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
...

var time by rememberSaveable { mutableStateOf("time") }
Column(
    modifier = Modifier.clickable { 
        time = LocalDateTime.now().toString() 
    }
) {
    Text(text = UUID.randomUUID().toString())
    Text(text = time)
}