ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Android] Compose Lifecycle(안드로이드 개발자 사이트 번역)
    안드로이드 2021. 8. 3. 17:08
    반응형

    개요

    Composable의 생명주기와 Recomposition이 필요한 Composable을 Compose가 어떻게 결정하는지 알아보자.

     

    Lifecycle

    State 관리에서 본것 처럼 Composition은 UI를 설명하고 Composable 실행에 의해 생성된다. Composition은 UI를 설명하는 Composable의 트리구조로 되어있다.

     

    Jetpacke Compose가 Composable들을 처음 실행할 때(Initial composition동안) Composition에서 UI를 설명하기 위해 호출하는 Composable을 추적한다. 그러고나서 앱의 State가 변경될때 Jetpack Compose는 Recomposition을 예약한다. Recomposition은 Jetpack Compose가 State 변경에 대한 응답이 변경된 Composable을 재실행하고 나서 변경사항을 반영하는 Composition을 업데이트 한다.

     

    Composition은 오직 Initial Composition에 의해 생성되고 Recomposition에 의해 업데이트 된다. Recomposition은 Compoition을 수정하는 유일한 방법이다.

     

    ** Composable의 lifecycle은 아래 event로 정의된다.

    • Composition 시작(Enter the Composition)
    • 0번 이상 재구성(Recompose 0 or more times)
    • Composition 종료(Leave the Composition)

    Recomposition은 일반적으로 State<T> Object가 변경에 의해 트리거된다. Compose는 이러한 Object를 추적하고 Composition에서 특정 State<T>를 읽는 모든 Composable 및 호출하는 Composable 중 건너뛸 수 없는 Composable을 실행한다.

     

    ** Composable의 Lifecycle은 View, Activity, Fragment의 Lifecycle보다 심플하다. Composable이 더 복잡한 Lifecycle을 가지는 외부리소스를 관리 또는 이와 상호작용이 필요하면 effects를 사용해야한다.

     

    만약 Composable이 여러번 호출되면, 여러개의 인스턴스는 Composition에 위치한다. Composable 호출에는 자신의 Lifecycle을 가지고 있다.

    @Composable
    fun MyComposable() {
        Column {
            Text("Hello")
            Text("World")
        }
    }

    위 그림은 MyComposable의 트리 구조이다. Composable이 여러번 호출되면 여러 인스턴스가 배치된다. 색상이 다른 요소는 별도의 인스턴스임을 나타낸다.

     

    Composition내의 Composable 분석

    Composition 내의 Composable 인스턴스는 call site에 의해 식별된다. Compose 컴파일러는 각 call site를 고유하다고 간주한다. 여러 call site로 부터 Composable을 호출하면 Composition 내에 여러 Composable 인스턴스를 생성한다. 

     

    ** call site : Composable이 호출되는 소스 코드 위치, Composition 내에서의 위치와 UI 트리에 영향을 미침

     

    만약 Recomposition 시 Composable이 이전 Composition 시 호출한 것돠 다른 Composable을 호출하는 경우, Compose는 호출된 것과 호출되지 않은 Composable을 식별하고 두 곳에서 모두 호출된 Composable의 경우 Compose에서 입력된 값이 변하지 않으면 재구성을 하지 않는다.

     

    ID를 유지하는 것은 Composable이 가지고있는 부수 효과와 연관있어서 매 재구성마다 재시작하는 것보다 성공적으로 완료할 수 있다.

     

    아래의 예제를 봐보자

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

    LoginScreen은 LoginError Composable을 조건부 호출하고 LoginInput Composable은 항상 호출하고 있다. 각 호출은 컴파일러에서 유니크하게 식별하는데 사용할 수 있는 유니크한 call site와 소스 위치를 가지고 있다. 

    LoginInput이 첫번째 호출된 후 두번째 호출될 때 LoginInput은 Recomposition에 걸쳐 유지될 것이다. 또한 LoginInput에는 Recomposition 간에  변경된 파라메터를 가지고 있지 않기 때문에 LoginInput은 Compose에 의해 건너 뛸 것이다.

     

    Smart Recomposition을 돕기 위한 정보 추가

    Composition을 여러번 호출하는 것은 Composition에 여러번 추가하는 것과 같다. Composable이 같은 call site에서 여러번 호출될 때 Compose는 각 호출마다 유니크한 ID에 대한 정보를 가지고 있지 않아서 인스턴스를 구분하기 위해 call site와 함께 실행 순서가 사용된다. 이 동작은 필요한 전부인 경우도 있지만 몇몇의 경우에는 원치 않는 동작의 원인이 될 수 있다.

    @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)
            }
        }
    }

    위의 예제를 보면 Compose는 Composition내에서 인스턴스를 구분하기 위해 call site와 함께 실행 순서를 사용한다. 만약 새로운 movie 가 리스트의 아래에 추가된다면 Compose는 Composition에 리스트에서 위치가 변하지 않고 movie 입력이 동일한 인스턴스이므로 인스턴스를 재사용할 수 있다.

    위 그림은 위 예제에 대한 Recomposition 그림이다. 색상이 같은 요소는 Recomposition 이 되지 않았음을 의미한다.

    MovieScreen에서 새로운 요소가 리스트의 아래에 추가된 그림이다. MovieOverview는 재사용 될 수 있다.

     

    그러나 movies 리스트가 리스트의 맨 위나 중간에 추가되는 변경이 있다면, 리스트 item을 삭제 또는 재배열 된다. 그러면서 위치가 변경된  모든 MovieOverview의 Recomposition이 발생한다. 이것은 예를들어 MovieOverview에서 영화 사진을 가져오는 부수 효과를 사용하는데 매우 중요하다. 만약 진행되는 동안 Recomposition이 발생하면 취소되고 다시 시작된다.

    @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)
    
            /* ... */
        }
    }

    위 그림은 리스트에 새 요소가 추가될 때의 MovieScreen을 보여준다. MovieOverview Composable은 다시 사용할 수 없고 부수 효과들은 다시 시작될 것이다. 다른 색상의 MovieOverview는 재구성된 Composable을 의미한다.

     

    이상적으로 MovieOverview 인스턴스의 ID는 인스턴스에 전달된 movie의 ID에 연결된 것으로 간주된다. movies 리스트를 재정렬한다면 각각의 MovieOverview가 다른 movie instance를 가진 Composable로 재구성되는 대신 Composition 트리에서 인스턴스를 재정렬하는 것이 이상적이다. Compose는 트리의 특정 부분에서 식별하는데 사용하기 원하는 값을 런타임에 지정할 수 있다.(key Composable)

     

    하나 이상의 값을 key Composable에 전달하여 호출하는 코드 래핑에 의해 이 값들은 인스턴스를 식별하는데 사용하기 위해 결합된 것이다. key의 값은 global에 유니크한 값일 필요는 없고, call site에서 Composable 호출시에만 유니크하면 된다. 아래 예제처럼 매 movie는 movies 사이에서 유니크한 key가 필요하다. key를 앱의 다른 위치에 있는 다른 Composable과 공유해도 괜찮다.

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

    리스트에서 요소가 변경되면 Compose는 알아채고 개별적으로 MovieOverview를 호출하고 재사용할 수 있다.

    위 그림은 MoviewScreen에서 새 요소가 리스트의 맨 위에 추가될 때를 보여준다. MovieOverview Composable이 유니크한 키를 가지고 있기 때문에 Compose는 MovieOverview가 변경되지 않았다는 것을 알아챌 수 있고 재사용할 수 있다.(부수 효과는 계속 실행된다.)

     

    ** key Composable을 사용하는 것은 composable 인스턴스를 식별하는데 도움을 준다. 이는 같은 call site에서 호출되고 부수 효과나 내부 state를 가지는 여러개의 Composable이 호출될 때 중요하다.

     

    몇몇 Composable은 key composable을 내부에 지원하고 있다. 예를들어 LazyColumn은 items DSL 내부에 특정 custom key를 받을 수 있다. 아래의 코드가 그 예제이다.

    @Composable
    fun MoviesScreen(movies: List<Movie>) {
        LazyColumn {
            items(movies, key = { movie -> movie.id }) { movie ->
                MovieOverview(movie)
            }
        }
    }

    입력이 변경되지 않은 경우 skip하기

    Composable이 Composition에 이미 있고 모든 입력이 안정적이고 변하지 않았다면 Recomposition을 건너 뛸 수 있다. 

    안정적인 타입은 아래의 계약을 준수해야한다.

    • 두 인스턴스의 equals 결과가 동일한 두 인스턴스는 항상 동일하다.
    • 만약 타입의 public 속성이 변경되었다면 Composition에 알린다.
    • 모든 public 속성 타입 또한 안정적이다.

    @Stable 어노테이션을 사용하여 명시적으로 안정적인 것으로 표시되지 않더라도 Compose 컴파일러가 안정적인 것으로 취급하는 이 계약에 속하는 몇가지 중요한 공통 타입이 있다.

    • 모든 주요 value 타입 : Boolean, Int, Long, Float, Char 등...
    • 문자열(Strings)
    • 모든 함수 타입(람다)

    이러한 모든 타입은 변경할 수 없기 때문에 stable 계약을 따를 수 있다. 변경할 수 없는 타입은 절대 변경할 수 없기 때문에 Composition에 변경사항을 전혀 알리지 않아도 되기 때문에 이 계약을 따르기 더 쉽다.

     

    안정적이고 변경가능한 하나의 주목할만한 타입은 Compose의 MutableState 타입이다. 만약 값이 MutableState에 유지되고 있다면, Compose가 State의 .value 속성의 변경사항에 대해 알림을 받을 것이기 때문에 State Object는 전반적으로 안정적이라고 간주된다.

     

    Composable에 파라메터로 전달되는 모든 타입이 안정적일 때, 파라메터 값은 UI 트리의 Composable 위치에 기반하여 동일한 값인지 비교된다. 모든 값이 이전 호출에서의 값과 변경된 것이 없다면 Recomposition은 skip된다.

     

    ** 요점 : Compose는 모든 입력 값이 안정적이고 변경되지 않았다면 Recomposition을 건너뛴다. 값 비교는 equals 메소드를 사용한다.

     

    Compose는 증명할 수 있을 경우 타입을 안정적이라고 간주한다. 예를 들어 interface는 일반적으로 다뤄지는 안정적이지 않다고 간주되며 변경할 수 있는 public 속성을 가지고 있고 구현을 변경할 수 없으므로 안정적이지 않다.

     

    만약 Compose가 타입이 안정적이라고 추론할 수 없지만 Compose가 해당 타입을 안정적으로 처리하도록 하려면 @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는 interface이기 때문에 Compose는 원칙적으로는 안정적이지 않은 타입으로 간주한다. @Stable 어노테이션을 추가함으로써 Compose에 이 타입이 안정적이라는 것을 알려줬고 Compose는 smart recomposition을 선호하게 된다. 즉 interface가 파라메터 타입으로 사용된다면 Compose는 이 interface의 모든 구현을 안정적으로 간주하고 처리하게 될 것이다.

     

    마치며

    Compose의 Lifecycle에 대해 알아보았다. View, Activity, Fragment의 생명주기보다는 간단하지만 성능을 좋게 개발하려면 Recomposition(재구성)에 대한 고민을 많이 해야할 것 같다. 다음에는 Side-effects(부수 효과)에 대해 알아보자.

    반응형

    댓글

Designed by Tistory.