ABOUT ME

설명이 필요해?

Today
Yesterday
Total
  • Composable의 수명주기
    IT/android 2024. 11. 21. 12:32
    SMALL

     

    컴포저블의 수명 주기

     

    초기 컴포지션단계 -> 리컴포지션 -> 컴포지션 종료

     

    컴포지션은 초기 컴포지션을 통해서만 생성되며 리컴포지션을 통해서만 업데이트 됩니다. 컴포지션을 수정하는 유일한 방법은 리컴포지션을 통하는 것입니다.

     

    리컴포지션은 State<T> 객체가 변경되면 트리거 됩니다. Compose 는 이러한 객체를 추적하고 컴포지션에서 특정 State<T> 를 읽는 모든 컴포저블 및 호출하는 컴포저블 중 건너뛸 수 없는 모든 컴포저블을 실행합니다.

     

    State<T> 객체의 설명은 다음과 같습니다.

    A value holder where reads to the value property during the execution of a Composable function, the current RecomposeScope will be subscribed to changes of that value.
    컴포저블 함수가 실행되는동한 읽을수 있는 값 홀더이며, 현재  RecomposeScope 가 값의 변화를 구독하게됩니다.

     

    Compose 를 이용하여 UI를 구성해본적이 있다면 mutableStateOf, as*State 같은 함수를 통해서 값을 저장하고, 가져오는 것을 해봤을 수가 있습니다. 이것들이 바로 State<T> 객체를 구현한 구현체 또는 확장함수들 입니다.

     

    컴포저블의 분석과 리컴포지션

    컴포지션 내의 컴포저블의 인스턴스는 *호출 사이트(Call site)로 식별됩니다. Compose 컴파일러는 각 호출 사이트를 고유한것으로 간주하고 여러 호출 사이트에서 컴포저블을 호출하면 컴포지션에 여러 컴포저블 인스턴스가 생성됩니다.

     

    *호출사이트는 컴포저블이 호출되는 소스코드의 위치 입니다. 호출 사이트는 컴포지션 내 위치와 UI 트리에 영향을 끼칩니다.

     

    리컴포지션 시 컴포저블이 이전 컴포지션에서 호출한것과 다른 컴포저블을 호출하는경우 Compose 는 호출되거나 호출되지않은 컴포저블을 식별하며 두 컴포지션 모두에서 호출된 컴포저블의 경우 입력이 변경되지않았다고 판단되면 리컴포지션을 하지 않습니다.

     

    이게 말로만 설명하면 잘 와닿지 않는데요, 관련 예제와 함께 본다면 이해하기 쉬울 것 같습니다.

    @Composable
    fun LoginScreen(showError: Boolean) {
        if (showError) {
            LoginError()
        }
        LoginInput() // This call site affects where LoginInput is placed in Composition
    }
    
    @Composable
    fun LoginInput() { /* ... */ }
    
    @Composable
    fun LoginError() { /* ... */ }

     

    이러한 컴포저블 함수들이 있을때, showError 상태에따라 LoginError() 컴포저블은 호출되거나 되지않습니다. 하지만 LoginInput() 컴포저블의 경우 입력을 변경할 매개변수가 없고, 두 상태 모두에 존재하는 컴포저블이기 때문에 리컴포지션이 발생하지 않습니다. 

     

    컴포저블을 여러번 호출하면 컴포저블이 컴포지션에도 여러번 추가가 됩니다. 동일한 호출 사이트에서 컴포저블을 여러번 호출하는 경우에는 Compose가 각 컴포저블 호출을 고유하게 식별할 수 있는 정보가 없기때문에 인스턴스들을 구분하기위해서 호출 사이트 이외에 실행 순서를 이용하여 구별을 하게 됩니다. 실행 순서를 이용한것으로 식별할수있는 경우도 있지만 그렇지 않는경우도 발생하여 불필요한 리컴포지션이 발생하기도 합니다. 다음 예제에서 살펴보도록 하겠습니다.

     

    @Composable
    fun MoviesScreen(movies: List<Movie>) {
        Column {
            for (movie in movies) {
                // MovieOverview composables are placed in Composition given its
                // index position in the for loop
                MovieOverview(movie)
            }
        }
    }
    
    @Composable
    fun MovieOverview(movie: Movie) {
        Column {
            // Side effect explained later in the docs. If MovieOverview
            // recomposes, while fetching the image is in progress,
            // it is cancelled and restarted.
            val image = loadNetworkImage(movie.url)
            MovieHeader(image)
    
            /* ... */
        }
    }

     

    위 예제는 Column 컴포저블에 에 MovieOverview 컴포저블 여러 인스턴스를 삽입하는 예제입니다.

    만약 movies 리스트에 마지막 인덱스에 movie 아이템이 추가되어 리컴포지션이 발생한다면 실행 순서를 이용하여 구별이 가능하기때문에 새로 추가된 MovieOverview(movie) 컴포지션 외에 다른 MovieOveview 인스턴스의 리컴포지션을 발생하지 않습니다.

    하지만 movies 리스트의 마지막 인덱스가 아니라 처음 또는 중간에 movie 아이템이 삽입된다면, 삽입된 아이템으로 인하여 인덱스 위치가 변경된 다른 아이템들은 모두 실행순서 기반의 구별을 할 수 없기때문에 모두 리컴포지션이 일어나게되어 성능 저하가 발생할 수 있고 의도치않은 결과가 발생할 수 있습니다.

     

    이를 방지하기 위해 또 다른 식별자를 추가할 수 있는데요, 바로 key 컴포저블 입니다.

    @Composable
    fun MoviesScreenWithKey(movies: List<Movie>) {
        Column {
            for (movie in movies) {
                key(movie.id) { // Unique ID for this movie
                    MovieOverview(movie)
                }
            }
        }
    }

     

     

    위 예제는 movie.id 를 키로 하여 MovieOverview 인스턴스를 생성하게 되는데요, 실행순서 대신 유니크한 key를 이용하여 식별하기때문에 movies 리스트 처음이나 중간에 데이터가 삽입되는 경우에도 식별이 가능하여 불필요한 리컴포지션이 발생하지 않게됩니다.
    주의할 점은 key 값은 반드시 유일한 값이어야 한다는 것입니다. 그래야 구별이 가능하겠죠?

     

    리컴포지션 건너뛰기(Skip recomposition)

    리컴포지션 중에 일부 요건을 충족하는 컴포저블 함수의 경우, 입력이 이전 컴포지션에서 변경되지 않았다면 리컴포지션을 건너뛸 수 있습니다. 

     

    Composable 함수는 다음을 제외하고 모두 건너뛸 수 있습니다.

    1. 함수에 Unit 가 아닌 반환 유형을 가지는 경우

    2. 함수가 @NonRestartableComposable 또는 @NonSkippableComposable 로 주석처리 된 경우

    3. 필수 매개변수가 안정적이지 않은 유형인 경우(un[Stable])

     

    어떠한 타입이 안정적인(Stable) 로 간주되기 위해서는 다음과 같은 조건이 필요합니다.

    1. 두 인스턴스의 equals 결과가 동일한 두 인스턴스의 경우 항상 동일해야합니다

    2. 타입의 public property 가 변경될 경우 컴포지션이 알림을 받을 수 있어야 합니다.

    3. 모든 public property 가 또한 Stable 이어야 합니다.

     

    @Stable 주석으로 처리되지 않은 경우에도 컴포즈 컴파일러가 안정적이라고 간주할 수 있는 중요한 일반 type 들이 있는데요,

    1. 모든 Primitive 타입들: Boolean, Int, Long, Float, Char,...

    2. Strings

    3. All function types (lambdas)

     

    이러한 타입들은 Stable 하다라고 간주될 수 있는데요, 왜냐하면 이것들은 Immutable 하기 때문입니다. Immutable 한 타입들은 변경될수 없기때문에 리컴포지션이 필요하다고 알리지 않습니다. 

     

    한가지 안정적이지만 변경이 가능한 예외가 있는데요, 바로 MutableState 입니다. 값이 변경되는 경우 State 의 value property 가 변경되면 Compose 에 알림이 되기때문에 MutableState 객체는 안정적인것으로 간주됩니다. 

     

    명시적 Stable

    Compose 는 위와같이 증명 가능한 경우에만 타입을 안정적인 것으로 간주하는데요, 예를들어 interface 의 경우 일반적으로는 안정적이지 않은것으로 간주되며, 구현을 변경할수 없으며  변경 가능한 public property 가 있는 타입도 안적적이지 않다고 간주됩니다.

    다만 명시적으로 안정적이다라는것을 컴파일러에게 알릴수는 있는데요, 바로 @Stable 주석을 이용하는 것 입니다.

    즉, 컴파일러가 추론은 할 수 없지만 개발자가 명시적으로 안정적이라고 알려주는 방법입니다.

     

    // Marking the type as stable to favor skipping and smart recompositions.
    @Stable
    interface UiState<T : Result<T>> {
        val value: T?
        val exception: Throwable?
    
        val hasError: Boolean
            get() = exception != null
    }
    위 UiState<T: Result<T>> 인터페이스의 경우 인터페이스이기때문에 일반적으로 안정적이지 않다고 추론을 하게 됩니다. 
    여기에 @Stable 주석을 추가하게 되면 컴포즈 컴파일러가 안정적인것을 알게되고 스마트 리컴포지션을 더 선호하게 됩니다. 즉 위 인터페이스가 매개변수 유형으로 사용된다면 컴파일러가 모든 인터페이스 구현체를 안정적으로 간주합니다.
    LIST
Designed by Tistory.