Compose 생명주기

Arakene·2024년 3월 31일

Compose 생명주기

  1. Init Composition
  2. Recomposition
  3. Leave Composition == DeComposition == onDispose

Init Composition

처음으로 Composable을 실행하여 Composition을 구성하는 작업을 뜻합니다.
remember을 통해 무언가 값을 저장하거나 계산하는 경우 이때 composition에 저장된다

Recomposition

composition을 변경할 수 있는 유일한 방법은 Recomposition을 통하는 방법뿐이다.
Recomposition은 0회에서 n회까지 재실행이 가능하다.
Recompostion의 주요한 트리거는 State<T>의 변경이다.
Recomposition이 일어나면 뷰를 다시 만들기에 무분별한 Recomposition은 성능을 저하시킬 수 있다.
Compose Compiler는 런타임시에 각각의 함수에 타입과 몇몇 태그를 할당한다. 이런 태그들은 컴파일러가 Recomposition을 어떻게 처리할지 결정한다.
함수에 붙는 마크로는 skippable or restartable이 붙을 수 있으며
타입으로는 Immutable, Stable이 존재한다.

Skippable

만약 Composable함수가 Skippable상태이면 모든 인자가 이전 상태와 변경점이 없다면 말그대로 Recomposition을 skip할 수 있다.
Skippable이 될 수 있는 조건으로는 모든 파라미터의 타입이 stable해야한다. 단 하나의 파라미터가 unstable인 경우 해당 함수는 unSkippable로 간주된다.

Restartable

composable이 restart할 수있는 스코프의 시작점이다. state가 변경되어 recomposition이 일어날때 compose가 재실행할 시작점을 의미한다.
@Composable함수는 Restartable이지만 skippable이 아닐 수 있다. 이와 같은 형태가 모든 케이스에서 이득이 없는 것은 아니다. 만약 자주 변경되는 State에 대해서 구독을 하고 있을때 not restartable하다면 해당 함수가 restartable하지 않기때문에 한단계 위의 부모의 함수를 recomposition시킬 것이다.
만약 어떤 함수가 recomposition트리의 root가 아니고 직접적으로 state에 대해서 구독하지 않으며, restart scope로써 사용될 일이 없다면 @NonRestartableComposable어노테이션을 추가해주는 것이 좋다. 컴파일러는 위 조건들에 대해서 구분하기 힘들기 때문에 어노테이션을 붙여주지 않는 한 매번 restart scope가 생성될 것이다.

Stable

Composable이 stable 파라미터로만 이루어진 경우 이전 스냅샷과 비교해서 차이점이 없으면 Recomposition을 스킵한다. Stable은 객체의 데이터가 변경될 수는 있지만 이전값과의 비교가 가능하다. 이때 비교는 Object.equels()를 사용해서 검사한다.

Unstable

Composeable이 이전 스냅샷과 비교할 수 없기에 Recomposition이 발생할 때마다 뷰를 재생성하게된다.
불필요한 Unstable 컴포넌트가 많을수록 많은 Recomposition이 발생할 것이고 이는 성능에 직결된다.
기본적으로 List, Map, Set와 같은 컬렉션들과 var로 선언된 친구들은 unstable로 분류된다.
만약 컬렉션이나 var에 대해서 immutable을 부여하고 싶다면 컬렉션은 Kotlinx Immutable Collections을 사용하면되며 두번째 방안으로는 어노테이션을 사용하는 방법이 있다.

@Stable과 @Immutable 어노테이션

stability 이슈를 해결하려면 이 두가지 어노테이션을 사용하는 방법도 존재한다.

다만 어노테이션을 사용하는것은 매우 신중하게 결정해야한다. 이 어노테이션을 사용했다고 해도 클래스자체를 immutable하거나 stable하게 만드는 것은 아니다. 그저 컴파일러에게 알려주는 것 뿐이다. 따라서 이런 어노테이션은 recomposition을 하지않거나 원치 않은 동작을 할 수 있다.

immutable collection을 사용할 수 없는 상태라면

@Immutable
data class ListWrapper(
	val list : ArrayList<String>
)

처럼 Wrapper를 사용해서 처리할 수도 있다.

Immutable Object

생성시에 값이 결정되며 이후로도 변경되지 않는 오브젝트를 뜻한다.
쉽게 말해서 생성자에 모두 val로 선언한 데이터 클래스를 생각하면된다. 아래의 User 데이터 클래스는 Immutable하다.

data class User(val name: String, val age: Int)

그 외에도 Immutable을 만족시키려면

  • Make sure all the class's properties are both val rather than var, and of immutable types.
  • Primitive types such as String, Int, and Float are always immutable.
  • If this is impossible, then you must use Compose state for any mutable properties.
    와 같은 조건을 만족해야한다.

Recomposition과 Skip이 일어나는지 확인해보자

LayoutInspector를 통해서 각각 몇회씩 수행되고 있는지 체크할 수 있다.

class와 function의 타입을 확인해보자

내가 만든 composable함수가 어떤 타입으로 마크되는지는 Compose compiler report를 통해서 받을 수 있다.
1. root의 build.gradle에 플래그 추가

subprojects {
  tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
        kotlinOptions {
            if (project.findProperty("composeCompilerReports") == "true") {
                freeCompilerArgs += [
                        "-P",
                        "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
                                project.buildDir.absolutePath + "/compose_compiler"
                ]
            }
            if (project.findProperty("composeCompilerMetrics") == "true") {
                freeCompilerArgs += [
                        "-P",
                        "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
                                project.buildDir.absolutePath + "/compose_compiler"
                ]
            }
        }
    }
}
  1. 터미널에서 아래 명령어 실행
./gradlew assembleRelease -PcomposeCompilerReports=true
  1. 결과 확인 -> build 디렉토리 아래에 있다.
  • -classes.txt: A report on the stability of classes in this module.
  • -composables.txt: A report on how restartable and skippable the composables are in the module.
  • -composables.csv:A CSV version of the composables report that you can import into a spreadsheet or processing using a script.

composables.txt의 경우

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun SnackCollection(
stable snackCollection: SnackCollection
stable onSnackClick: Function1<Long, Unit>
stable modifier: Modifier? = @static Companion
stable index: Int = @static 0
stable highlight: Boolean = @static true
)

와 같이 해당함수가 restartable, skippable한지와 파라미터들의 타입에 대해서 알 수 있다.

classes.txt의 경우

unstable class Snack {
stable val id: Long
stable val name: String
stable val imageUrl: String
stable val price: Long
stable val tagline: String
unstable val tags: Set<String>
<runtime stability> = Unstable
}

와 같이 클래스에 대한 정보를 알 수 있다.

LifeCycle과의 연동

onStart, onResume, onPause, onStop과 같이 lifecycle과 연동해서 작업을 수행해야할 때가 있다.
기존에도 lifecycle을 받아와서 커스텀 함수를 만들 수 있었지만 이제는 정식적으로 지원을 해준다.

준비물

그래들에 종속성 추가

implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.7.0'

지원하는 친구들

LifecycleEventEffect

인자로 어느 Lifecycle.Event에서 실행할 것인지, 어떤 작업을 실행할 것인지를 받는다. lifecycleowner경우 기본값으로 local을 가져오지만 따로 설정해줄 수도 있다.

다만 onDestroy 이벤트를 설정하면 오류가난다. 그 이유로는 compose는 onStop이벤트를 받으면 recomposition을 멈추게 되며 onDestory를 감지하지못해 인자로 넘겨준 Event를 실행하지 못하기 때문이다.

그리고 한가지 더 하지말라는게 있는데

This function should also not be used to launch tasks in response to callback events by way of storing callback data as a Lifecycle.State in a MutableState. Instead, see currentStateAsState to obtain a State<Lifecycle.State> that may be used to launch jobs in response to state changes.

외부에서 따로 MutableState<Lifecycle.State>를 가지고있는 상태에서 LifecycleEventEffect의 event로 외부의 state를 변경시켜 뭔가 작업을 실행시키는 것은 하지말라는 것 같다. 대신에

    val lifecycleOwner = LocalLifecycleOwner.current
    val currentLifecycleState = lifecycleOwner.lifecycle.currentStateAsState()

을 사용하는 것을 요구하는 것 같다. 이때 currentStateAsState()는 임포트해줘야 사용이 가능하다.

내부적으로는 DisposableEffect로 구현되어있으며 key로는 lifecycleOwner가 지정되어있기에 owner가 사라지는 순간 effect실행 시 등록한 LifecycleEventObserver를 제거한다.

LifecycleStartEffect

LifecycleEventEffect 와 동일하게 effect실행 시 LifecycleEventObserver를 등록하지만 onStart에서 작업을 시작하도록하고 onStop일 때는 LifecycleEventObserver를 제거하는 방식이다.

  • LifecycleResumeEffect
    'LifecycleStartEffect'와 동일한 구조로 onResume과 onPause가 커플로 생성되는 것 말고는 차이점이 없다.
profile
안녕하세요 삽질하는걸 좋아하는 4년차 안드로이드 개발자입니다.

0개의 댓글