Overview
Composition은 내 앱의 UI를 보여주며, composables의 실행에 의해 생성된다. 다시 말해 Composition은 UI를 그리는 composables의 트리 구조다.
제트팩 compose는 composition이 1) 처음 실행될 때, composables 함수를 실행시키는데 이후 Composition에 있는 UI를 그리기 위해 composables를 계속 감시한다. 2) 변화를 감지하면 제트팩 compose는 recompostion를 스케줄링한다. recomposition은 상태 변화에 대한 반응으로 composables을 바꿀 때 일어난다. 다시 말해 내 composable를 수정할 수 있는 경우는 recomposition이 일어날 때뿐이다.
그림은 Composable의 lifecycle이다. 우선 composition에 들어간 뒤, 0번 이상 recompose가 일어난 뒤 composition을 떠난다. recomposition은 전형적으로 State<T> 객체에 의해 트리거되는데, compose는 이 객체를 추적한다.
@Composable
fun MyComposable(){
Column {
Text("Hello")
Text("World")
}
}
composable함수가 여러 번 호출되면 그만큼 해당 인스턴스가 composition에 생성된다. 호출 시 composition에서 각자의 lifecycle을 가진다. 여기서 서로 다른 색깔은 다른 인스턴스라는 것을 알려준다.
Composition 속 composable의 구조 파헤치기
Composition의 composable 인스턴스는 호출된 영역에 한해 구별될 수 있다. Compose 컴파일러가 각 호출 영역마다 구분시키기 때문이다. 여러 호출 영역으로부터 호출된 composable들은 여러 인스턴스를 생성할 수 있다.
호출 영역은 composable이 호출된 소스코드 영역이다.
recomposition 도중 이전 composition과 다른 composition을 호출하는 경우 Compose는 두 composition 중 어떤 composable이 호출됐는지 확인한다. 그리고 compose는 두 composition의 input의 차이가 없는지 확인하고 차이가 없다면 recomposition를 하지 않는다.
identity를 유지하여 recomposition 시 모든 걸 재시작하지 않고 composable과 side effect를 연관시킨다.
@Composable
fun LoginScreen(showError : Boolean){
if(showError) {
LoginError()
}
LoginInput() // LoginInput이 있는 composition에 영향을 준다
}
@Composable
fun LoginInput() { /* ... */ }
@Composable
fun LoginError() { /* ... */ }
위 코드에서 LoginScreen()은 조건부로 LoginError를 호출하고 LoginInput()은 항상 호출한다. 모든 호출은 자기 만의 유니크한 호출 영역을 가지고 있고 컴파일러도 둘을 구변하기 위해 사용한다.
LoginSreen은 state가 변화할 때, recompostiton이 발생할 때 화면에 표시한다. 여기서 같은 컬러는 recompose가 일어나지 않았다는 뜻이다.
loginInput이 두번째 호출되어도 LoginInput 인스턴스는 recomposition될 때 쭉 유지된다. 이는 LoginInput가 recomposition으로 변화하는 매개변수가 없기 때문에 호출 자체를 생략한다.
smart Recomposition
여러 번 composable을 호출하면 compostion에도 여러 번 추가된다. 같은 호출 영역에서 composable을 여러번 호출하면 composable의 매 호출을 구분하지 못한다. 그래서 실행 순서를 통해 호출을 구분한다. 호출 순서로 인스턴스를 구분한다.
@Composable
fun MoviewScreen(movies: List<Movie>){
Column {
for (movie in movies) {
MovieOverview(movie)
}
}
}
1) composable이 하단에 추가될 때
리스트의 마지막에 새로운 요소가 추가됐을 때의 MoviewScreen 그림이다. MoviewOverview의 색깔이 동일한 건 recompose가 되지 않았다는 뜻이다. 상단, 가운데 혹은 삭제 및 재정렬되는 경우 recomposition된다. side-effect를 사용하면 recomposition이 발생됐을 때 진행이 취소되고 다시 시작된다.
@Composable
fun MoviewOverview(movie: Movie){
Column {
val image = loadNetworkImage(movie.url)
MovieHeader(image)
}
}
2) composable이 상단에 추가될 때
새 요소가 상단에 추가될 때 MoviewOverview composable은 재사용될 수 없으며, 모든 side effect는 재시작한다. MoviewOverview의 서로 다른 컬러는 recompose됐다는 의미다.
3) composable이 재정렬될 때
MoviewOverview의 인스턴스의 id가 해당 인스턴스에 전달되는 movie의 id에 의해 구별될 수 있기에 movie list의 순서가 바뀐다면 moviewOverview를 recompose하지 않고 Composition 트리에 있는 MoviewOverview의 순서를 바꿀 수 있다. compose는 런타임동안 tree의 moive들을 구별하기 위해 key composable을 사용한다.
key composable
하나 이상의 값이 호출된 key composable과 코드블럭이 랩핑되면 이런 값들은 composition에서 인스턴스를 구별하기 위해 결합된다. key값은 전역적으로 사용할 필요가 없으며 각 호출사이트에서만 고유하면 된다.
@Composable
fun MoviesScreenWithKey(movies: List<Movie>){
Column {
for (movie in movies) {
key(movie.id) { 해당 movie에 대한 고유 id
MoviewOverview(movie)
}
}
}
}
각 movie는 movie list에서 각자의 key가 있다.
위 코드에 따르면 각 요소가 리스트의 상단에 추가되더라도 Compose는 개별 호출을 인지하고 재사용할 수 있다.(이전에 key가 없었다면 재사용할 수 없었다)
Compose는 각 MoviewOverview 인스턴스가 각자의 key를 가지고 있다면 식별할 수 있고 이 side effect는 계속해서 실행된다.
일부 composable(e.g. LazyColumn)은 key composable 기능이 내장되어 있다.
@Composable
fun MoviewScreenLazy(movies : List<Movie>){
LazyColumn {
items(movies, key = { moview -> movie.id }) { movie ->
MovieOverview(movie)
}
}
}
입력이 변경되지 않은 경우엔 스킵하기
재구성 중 어떤 composable 함수는 이전 composition과 비교했을 때 바뀌지 않았다면 실행되지 않는다. 하지만 다음의 경우엔 스킵할 수 없다.
- 함수의 리턴 타입이 unit타입이 아닌 경우
- 함수의 어노테이션이 @NonRestartableComposable 혹은 @NonSkippableComposable인 경우
- 요구한 파라미터가 stable 타입이 아닌 경우
여기서 stable 타입이라고 간주하기 위해 다음 조건을 따라야 한다.
- 두 인스턴스의 equal이 항상 동일해야 한다.
- public 속성의 타입이 바뀌면 composition에 알림이 간다.
- 모든 public 속성은 모두 stable하다.
다음은 compose컴파일러가 stable이라고 다룰 수 있게 하는 공통적인 타입이다. 만약 stable하지 않다면 @Stable 어노테이션을 사용할 수 있다. @Stable이외에 다음의 경우도 stable이라고 판단한다. 이 table 타입은 불변하기 때문에 Composition이 감지할 필요도 없다. (모든 변경불가능한 타입은 stable타입이다)
- 기본 값 유형 : Boolean, int, Long, Float, Char...
- String
- 함수 타입
주목할 만한 stable타입은 Compose의 MutableState 타입이다. 이 객체는 변경가능하지만 stable해서 value 속성을 통해 compose가 감지할 수 있다.
composable에 파라미터로 전달되는 타입이 stable하다면 그 파라미터의 값은 UI 트리의 cmoposable 위치를 기준으로 동일한지 확인하게 된다. 여기서 이전 composable의 위치를 비교하여 매개변수가 동일한지 확인, 동일하다면 recomposition을 스킵한다. 이때 비교연산으로 equals 메소드를 사용한다.
Compose는 증명할 수 있는 경우에만 해당 타입을 stable이라고 인신한다. 예를 들면, 한 인터페이스가 stable하지 않다고 고려된다면 변화가능한 퍼블릭 속성 역시 stable하다고 판단한다. 만약 Compose이 stable할 수 있는지 확인할 수 없다면 Compose에게 @Stable 어노테이션을 붙여 강제로 만들 수 있다.
@Stable
interface UiState<T : Result<T>> {
val value: T?
val exception: Throwable?
val hasError: Boolean
get() = exception != null
}
Uistate은 stable하지 않기 때문에 @Stable 주석을 추가하여 smart recomposition을 진행한다.
'모바일 > Android' 카테고리의 다른 글
[공식 문서] compose 단계 (0) | 2024.08.06 |
---|---|
[공식문서] Side-effects in Compose (0) | 2024.08.04 |
Kotlin Coroutine (0) | 2024.07.02 |
Suspicious indentation: This is indented but is not continuing the previous expression (0) | 2024.06.15 |
SQLite (0) | 2024.06.14 |
댓글