본문 바로가기

안드로이드 Android

안드로이드 개발 (10) Compose Composable Lifecycle

안녕하세요 Loner 입니다. 오늘은 Compose의 컴포저블 라이프사이클 공부를 정리해 봤습니다.

 

읽기전에 알아야할 개념-

Composition, ReComposition

https://gift123.tistory.com/34

 

안드로이드 개발 (9) Compose 상태 관리

jetpack compose 에 한창 포스팅 중입니다. https://gift123.tistory.com/33 안드로이드 개발 (8) Compose 이해 정리 이번 포스팅부터 Compose에 대해 차근히 파헤쳐 가보겠습니다. Android Compose 공식 문서를..

gift123.tistory.com

 

위 링크에서 언급한대로 Composition은 UI를 기술하는 컴포저블의 트리 구조입니다.

 

 

 Compose 최초 Composition 시의 순서입니다.

1) 처음으로 컴포저블을 실행

2) UI를 기술하기 위해 호출하는 컴포저블을 추적

3) 앱 상태가 변경되면 Jetpack Compose는 recomposition을 예약

4) recompostion은 Compose가 상태 변경사항에 따라 Composable을 다시 실행해서 컴포지션을 업데이트

 

*composition은 초기 composition을 통해서만 생성, recomposition을 통해서만 업데이트 가능*

 

 

1. Composable Lifecycle

Composable의 라이프 사이클은

컴포지션 시작, 0회 이상 recomposition 및 컴포지션 종료 이벤트로 정의

순서: Composable이 Composition을 시작 ->        

            0회 이상 recomposition ->

               comosition을 종료

 

1)

Composable 의 Lifecycle는 View, Activity 및 Fragment의 Lifecycle보다 간단합니다.

Composable 은 Lifecycle 이 더 복잡한 외부 리소스를 관리하거나 이와 상호작용해야 하는 경우 효과를 사용 해야함

 

2)

Composable이 여러 번 호출되면 Composition에 여러 인스턴스가 배치됩니다.

Composition 각 호출에는 자체 Lifecycle가 있습니다.

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

 

3)

recomposition은 일반적으로 State<T> 객체가 변경되면 트리거됩니다.

Compose는 이러한 객체를 추적합니다.

 

아래 두개중에 건너뛸 수 없는 모든 컴포저블을 실행

(1)Composition에서 특정 State<T>를 읽는 모든 Composable

(2)호출하는 Composable 

 

 

2. 컴포지션 내 컴포저블의 분석

*호출 사이트는 컴포저블이 호출되는 소스 코드 위치*

-Composition 내 composable 의 인스턴스는 호출 사이트로 식별

-Compose 컴파일러는 각 호출 사이트를 고유한 것으로 간주

-여러 호출 사이트에서 컴포저블을 호출하면 컴포지션에 컴포저블의 여러 인스턴스가 생성

 

하지만..

recompostion 시 composable이 이전 recomposition 시 호출한 것과 다른 composable을 호출하는 경우

 

1) Compose는 호출되거나 호출되지 않은 composable을 식별

2) 두 Compostion 모두에서 호출된 composable의 경우 입력이 변경되지 않은 경우 recomposition 하지않음

 

부수 효과를 composable과 연결하기 위해서는 ID를 유지하는 것이 중요함 

(recomposition마다 다시 시작하는 대신 완료할 수 있도록)

 

예제

@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을 호출합니다.

 

각 호출에는 고유한 호출 사이트 및 컴파일러가 호출을 고유하게 식별하는 데

사용하는 소스 위치가 있습니다.

 

상태가 변경되고 recomposition이 발생할 때 composition 내 LoginScreen의 표현.

(색상이 동일하면 recomposition되지 않았음을 의미합니다.)

 

LoginInput이 첫 번째로 호출되었다가 두 번째로 호출되었지만

LoginInput 인스턴스는 여러 recomposition에 걸쳐 유지됩니다.

 

또한 LoginInput에는 recomposition 간에 변경된 매개변수가 없으므로

Compose가 LoginInput 호출을 건너뜁니다.

 

2-1.스마트 recomposition에 도움이 되는 정보 추가

-동일한 호출 사이트에서 composable을 여러 번 호출하는 경우 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)
        }
    }
}

위 예에서 Compose는 호출 사이트 외에 실행 순서를 사용하여

Composition 에서 인스턴스를 구분합니다.

 

 movie가 리스트의 하단에 추가된 경우 

ex: list.add(movie)

 

Compose는 인스턴스의 리스트

위치가 변경되지 않았고

인스턴스의 movie 입력이 동일하므로

컴포지션에 이미 있는 인스턴스를 재사용할 수 있습니다.

 

(ex: {1,2,3}일때 list.add(4) 를 하면  {1,2,3,4} 라서

 이전과 비교해서 4를 제외한 1,2,3 값과 index 위치가 그대로 있는거라면 )

위 그림처럼 composition 의 MovieOverview Composable은 재사용이 됩니다.

(색상이 동일하면 recomposition되지 않았음을 의미합니다.)

 

 

 

 

하지만....

 

리스트의 상단 아이템 추가, 리스트의 가운데 아이템 추가, 항목을 삭제, 재정렬 등등

movies 목록이 변경되면 목록에서 입력 매개변수의 위치가

변경된 모든 MovieOverview Composable 호출에서 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)

        /* ... */
    }
}

 

위 그림은 ex:list.add(0,movie) 를 했을때의 예시입니다. 

(색상이 동일하지 않으면 recomposition 되었음을 의미합니다.)

- 재사용 없이 다시 recompostion이 됨

- 모든 부수효과가 다시 시작됨

 

그렇다면 위와 상황에서 recomposition 없이 재사용을 사용할려면 

이상적으로 MovieOverview 인스턴스의 ID는

인스턴스에 전달된 movie의 ID에 연결되는것이 좋습니다.

 

Compose에서 런타임에 트리의 특정 부분을

식별하는 데 사용할 Key를 지정할 수 있습니다.

 

 key 값은 전체적으로 고유하지 않아도 되며

호출 사이트에서의 Composable 호출 간에만 고유하면 됩니다.

(앱의 다른 위치에 있는 다른 Composable과 이 key를 공유하는 것은 괜찮습니다.)

 

예제

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

 

movies의 요소가 변경되더라도 Compose는

key별로 MovieOverview() 호출을 인식하고 재사용할 수 있습니다.

 

Key를 사용했기 때문에 고유 식별자가 생겨서 recomposition 없이 재사용이 가능해집니다.

(색상이 동일하면 recomposition되지 않았음을 의미합니다.)

 

일부 Composable에는 key Composable 지원 기능이 내장되어 있습니다.

 

예제

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

예를 들어 LazyColumn의 경우 items DSL에 맞춤 key를 지정할 수 있습니다.

 

2-2.입력이 변경되지 않은 경우 건너뛰기

Composition에 이미 Composable이 있는 경우 모든 input이 안정적이고 변경되지 않았으면

recompostion을 건너뛸 수 있습니다.

 

Stable 타입은 다음을 준수해야 합니다.

 

1) 두 인스턴스의 equals 결과가 동일한 두 인스턴스의 경우 항상 동일

2) 타입의 Public 속성이 변경되면 Composition에 알림이 전송

3) 모든 Public 속성 유형도 안정적

 

예외로 안정적이지만 변경할 수 있는 한 가지 중요한 타입은 Compose의 MutableState

value가 MutableState로 유지되는 경우 State의 .value 속성이 변경되면 Compose에 알림이 전송합니다.

그래서 상태 객체는 안정적인 것으로 간주됩니다.

 

Composable에 매개변수로 전달된 모든 타입이이 안정적인 경우

UI 트리 내 Composition 위치를 기반으로 매개변수 값이 동일한지 비교합니다.

(이전 호출 이후 모든 값이 변경되지 않은 경우 recompostion 을 건너뜀)

 

@Stable

Compose는 증명할 수 있는 경우에만 유형을 안정적인 것으로 취급

(ex: 인터페이스는 일반적으로 안정적이지 않은 것 취급되며 구현을

변경할 수 없고 변경할 수 있는  속성이 있는 유형도 안정적이지 않음)

 

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
}

 

인터페이스는 이 타입을 안정적이지 않은 것으로 간주함

하지만 @Stable 주석을 추가하면 Compose가 이 유형이 안정적임을 알게 되고 스마트 recomposition을

선호하게 됨

 

인터페이스가 매개변수 유형으로 사용되는 경우

Compose가 모든 구현을 안정적인 것으로 간주함

 

@Stable 주석을 사용하여 안정적이라고 명시되지 않더라도

Compose 컴파일러가 안정적인 것으로 간주하며 이 계약에 포함되는 중요한 일반 타입이 있습니다.

 

1) 모든 원시 값 타입: Boolean, Int, Long, Float, Char 

2) 문자열

3) 모든 함수 타입(람다)

 

- 이 타입은 모두 변경할 수 없으므로 stable 계약을 준수함

- 변경할 수 없는 유형은 절대 변경되지 않으므로 Composition에 변경사항을 알리지 않아도 됨

 

 

이상 Composable의 Lifecycle 이었습니다. 

개발 툴은 항상 더 좋은 편리함을 위해 발전합니다. Compose 는 더 좋은 성능을 낼 수 있는 UI 제작 툴이라 생각드네요

이어서 더 Compose를 깊게 알아가야겠습니다.