강력 건너뛰기 모드

강력한 건너뛰기는 Compose 컴파일러에서 사용할 수 있는 모드입니다. 사용 설정하면 컴파일러의 동작이 다음 두 가지 방식으로 변경됩니다.

강력한 건너뛰기 모드 사용 설정

강력한 건너뛰기는 Kotlin 2.0.20에서 기본적으로 사용 설정되어 있습니다.

2.0.20 이전 출시 버전에서 Gradle 모듈에 강력한 건너뛰기 모드를 사용 설정하려면 Gradle 구성의 composeCompiler 블록에 다음 옵션을 포함합니다.

android { ... }

composeCompiler {
   enableStrongSkippingMode = true
}

컴포저블 건너뛰기 가능 여부

강력한 건너뛰기 모드는 건너뛰기 및 구성 가능한 함수와 관련하여 Compose 컴파일러에서 일반적으로 적용하는 일부 안정성 규칙을 완화합니다. 기본적으로 Compose 컴파일러는 모든 인수에 안정적인 값이 있는 경우 구성 가능한 함수를 건너뛸 수 있는 것으로 표시합니다. 강력한 건너뛰기 모드는 이 동작을 변경합니다.

스마트 건너뛰기를 사용 설정하면 다시 시작 가능한 모든 구성 가능한 함수를 건너뛸 수 있습니다. 이는 불안정한 매개변수가 있는지 여부와 관계없이 적용됩니다. 다시 시작할 수 없는 구성 가능한 함수는 건너뛸 수 없는 상태로 유지됩니다.

건너뛰어야 하는 경우

리컴포지션 중에 컴포저블을 건너뛸지 결정하기 위해 Compose는 각 매개변수의 값을 이전 값과 비교합니다. 비교 유형은 매개변수의 안정성에 따라 다릅니다.

  • 불안정한 매개변수는 인스턴스 동등성 (===)을 사용하여 비교됩니다.
  • 안정적인 매개변수는 객체 동등성 (Object.equals())을 사용하여 비교됩니다.

모든 매개변수가 이러한 요구사항을 충족하면 Compose는 리컴포지션 중에 컴포저블을 건너뜁니다.

컴포저블에서 강력한 건너뛰기를 선택 해제할 수 있습니다. 즉, 다시 시작할 수 있지만 건너뛸 수 없는 컴포저블을 원할 수 있습니다. 이 경우 @NonSkippableComposable 주석을 사용합니다.

@NonSkippableComposable
@Composable
fun MyNonSkippableComposable {}

클래스에 안정적인 것으로 주석 달기

인스턴스 동등성 (===) 대신 객체 동등성 (Object.equals())을 사용하여 객체를 비교하려면 지정된 클래스에 @Stable 또는 @Immutable로 계속 주석을 추가합니다.

@Stable 또는 @Immutable를 추가하기 전에 다음 장단점을 고려하세요.

  • Compose 모듈에서 안정적인 속성이 있는 클래스는 이미 안정적임: Compose 컴파일러로 컴파일된 모듈에서 안정적인 유형의 val 속성이 있는 클래스는 자동으로 안정적인 것으로 추론되며 @Immutable 또는 @Stable 없이 .equals()를 사용합니다.
  • 저렴한 .equals() 검사로 클래스에만 주석 달기: 컴파일러가 안정성을 추론할 수 없고 (예: Compose가 아닌 모듈의 클래스) 동일한 값을 가진 새 인스턴스가 자주 할당되는 경우 @Stable 또는 @Immutable를 추가합니다. 단, 클래스의 .equals() 검사가 저렴해야 합니다.
  • 컬렉션으로 모델에 주석을 달 때 주의: 컬렉션이 포함된 모델에 @Stable 또는 @Immutable를 추가하거나 List를 ImmutableList로 바꾸면 Compose는 각 리컴포지션 패스에서 컬렉션의 모든 요소를 .equals()와 비교합니다. 지연 레이아웃 (LazyColumn, LazyRow, LazyGrid)에 사용되는 대규모 컬렉션이나 모델의 경우 모든 요소를 비교하는 것이 인스턴스 동등성 (===)을 사용하거나 직접 재구성하는 것보다 느릴 수 있습니다.

람다 메모화

강력한 건너뛰기 모드를 사용하면 컴포저블 내에서 람다의 메모이제이션도 더 많이 사용할 수 있습니다. 강력한 건너뛰기를 사용 설정하면 컴포저블 함수 내의 모든 람다가 자동으로 기억됩니다.

예

강력한 건너뛰기를 사용할 때 컴포저블 내에서 람다의 메모화를 달성하기 위해 컴파일러는 remember 호출로 람다를 래핑합니다. 람다의 캡처로 키가 지정됩니다.

다음 예와 같이 람다가 있는 경우를 생각해 보세요.

@Composable
fun MyComposable(unstableObject: Unstable, stableObject: Stable) {
    val lambda = {
        use(unstableObject)
        use(stableObject)
    }
}

강력한 건너뛰기를 사용 설정하면 컴파일러는 람다를 remember 호출로 래핑하여 메모합니다.

@Composable
fun MyComposable(unstableObject: Unstable, stableObject: Stable) {
    val lambda = remember(unstableObject, stableObject) {
        {
            use(unstableObject)
            use(stableObject)
        }
    }
}

키는 컴포저블 함수와 동일한 비교 규칙을 따릅니다. 런타임은 인스턴스 동등성을 사용하여 불안정한 키를 비교합니다. 객체 동등성을 사용하여 안정적인 키를 비교합니다.

메모이제이션 및 리컴포지션

이 최적화를 통해 런타임이 리컴포지션 중에 건너뛰는 컴포저블의 수가 크게 증가합니다. 메모화가 없으면 런타임은 리컴포지션 중에 람다 매개변수를 사용하는 컴포저블에 새 람다를 할당할 가능성이 훨씬 더 높습니다. 따라서 새 람다에는 마지막 컴포지션과 같지 않은 매개변수가 있습니다. 이로 인해 재구성이 발생합니다.

메모이제이션 방지

메모화하지 않으려는 람다가 있는 경우 @DontMemoize 주석을 사용합니다.

val lambda = @DontMemoize {
    ...
}

APK 크기

컴파일 시 건너뛸 수 있는 컴포저블은 건너뛸 수 없는 컴포저블보다 더 많은 코드를 생성합니다. 강력한 건너뛰기를 사용 설정하면 컴파일러는 거의 모든 컴포저블을 건너뛸 수 있는 것으로 표시하고 모든 람다를 remember{...}로 래핑합니다. 따라서 강력한 건너뛰기 모드를 사용 설정해도 애플리케이션의 APK 크기에 미치는 영향은 매우 적습니다.

Now In Android에서 강력한 건너뛰기를 사용 설정하면 APK 크기가 4kB 증가했습니다. 크기 차이는 주로 지정된 앱에 있었던 이전에 건너뛸 수 없는 컴포저블의 수에 따라 다르지만 상대적으로 작아야 합니다.