[정리] Compose 가이드 문서 ~ Performance

[정리] Compose 가이드 문서 ~ Performance

Jun 19, 2024. | By: pluulove


https://developer.android.com/develop/ui/compose/performance


성능 ~ 개요

앱을 올바르게 구성하기

  • R8 + 릴리스 모드에서 빌드
  • Baseline Profiles 사용

Compose 단계와 성능

Compose가 프레임을 업데이트할 때의 단계

  • Composition : 표시할 내용을 결정. composable 함수를 실행, UI 트리를 빌드
  • Layout : UI 트리에 있는 각 컴포넌트의 크기/배치를 결정
  • Drawing : 실제로 개별 UI 컴포넌트를 렌더링

Compose는 필요하지 않은 경우 각 단계를 건너뛸 수 있다.

일반 원칙

  • 가능하면 Composable 함수에서 계산을 이동하세요.
    • UI가 변경될 때마다 Composable 함수를 다시 실행해야 할 수도 있다. Composable에 넣은 모든 코드는 잠재적으로 애니메이션의 모든 프레임에 대해 다시 실행된다. UI를 빌드하는 데 필요한 것만으로 Composable의 코드를 제한
  • 가능한 한 오랫동안 상태 읽기를 연기합니다.
    • 상태 읽기를 하위 Composable 또는 이후 단계로 이동하면 Recomposition을 최소화하거나 Composition 단계를 완전히 건너뛸 수 있다.
    • 자주 변경되는 상태에 대해 상태 값 대신 람다 함수를 전달하고, 자주 변경되는 상태를 전달할 때 람다 기반 Modifier로 대응할 수 있다.

Baseline Profiles

Baseline Profiles은 포함된 코드 경로에 대한 해석 및 JIT(Just-In-Time) 컴파일 단계를 방지하여 첫 번째 실행보다 코드 실행 속도를 향상시킨다. 앱이나 라이브러리에 기본 프로필을 제공하면 ART(Android 런타임)를 활성화하여 AOT(Ahead-of-Time) 컴파일을 통해 포함된 코드 경로를 최적화하여 모든 새 앱 설치 및 모든 앱 업데이트에 대한 성능 향상을 제공할 수 있다

성능 고려사항 작성

  • Compose는 Android 플랫폼의 일부가 아닌 라이브러리로 배포. Compose를 자주 업데이트하고 다양한 Android 버전을 지원할 수 있다.
    • Compose를 라이브러리로 배포하면 비용이 발생
  • Android 플랫폼 코드는 이미 컴파일되어 기기에 설치되어 있다. 그러나, 라이브러리는 앱이 시작될 때 로드+필요할 때 JIT를 해석
    • 앱이 시작될 때와 처음으로 라이브러리 기능을 사용할 때 앱 속도가 느려질 수 있다.
    • Bundle/Unbundle

Baseline Profiles 이점

프로필은 중요한 사용자 여정에 필요한 클래스와 메소드를 정의하고 앱의 APK/AAB와 함께 배포.

-> 앱 설치 중에 ART는 앱이 실행될 때 사용할 수 있도록 이 중요한 코드 AOT를 컴파일

-> Compose와 함께 제공되는 Baseline Profiles에는 Compose 라이브러리 내의 코드에 대한 최적화만 포함

Macrobenchmark

Macrobenchmark를 사용하여 Baseline Profiles을 만들 수 있다

  • Compose UI용 Macrobenchmark 테스트를 작성하는 방법 : https://github.com/android/performance-samples/tree/main/MacrobenchmarkSample

Stability ~ 개요

Immutable 객체

모든 매개변수가 val 키워드로 정의된 Primitive 타입

data class Contact(val name: String, val number: String)

@Composable
fun ContactRow(contact: Contact, modifier: Modifier = Modifier) {
  var selected by remember { mutableStateOf(false) }

  Row(modifier) {
     ContactDetails(contact)
     ToggleButton(selected, onToggled = { selected = !selected })
  }
}

ToggleButton 선택시 상태변경 모습

  1. Compose는 ContactRow 내에서 코드를 recomposition해야 하는지 평가
  2. ContactDetails의 파라미터가 Contact 유형임을 확인
  3. Contact는 Immutable data 클래스이므로 Compose는 ContactDetails의 파라미터가 변경되지 않았음을 확인
  4. Compose는 ContactDetails를 건너뛰고 recomposition skip
  5. 반면에 ToggleButton의 파라미터가 변경되었으며 Compose는 이 컴포넌트를 recomposition

Mutable 객체

data class Contact(var name: String, var number: String)

속성이 변경되면 Compose가 인식하지 못함 -> Compose가 Compose 상태 객체의 변경사항만 추적하기 때문

불안정한 클래스의 recomposition을 건너뛰지 않음

-> ContactRow는 selected가 변경될 때마다 recomposition 발생

Compose에서 구현

Compose 컴파일러가 코드에서 실행되면 각 함수/타입을 태그로 표시. 이 태그는 Compose가 recomposition 중에 함수/타입을 처리하는 방식을 반영

함수

skippable/restartable로 표시

  • skippable : 인수가 이전 값과 같은 경우 Compose는 recomposition 중에 composable skip 할 수 있다
  • restartable : restartable 있는 composable은 recomposition을 시작할 수 있는 ‘범위’ 역할
    • 상태가 변경된 후 Compose가 recomposition을 위한 코드 재실행을 시작할 수 있는 진입점

타입

Immutable/Stable로 표시

  • Immutable : 값을 변경할 수 없고 모든 메서드가 참조적으로 투명한 경우, Immutable로 표시
    • primitive types : 모두 Immutable 표시 (String, Int, Float)
  • Stable : 생성 후 속성이 변경될 수 있는 타입. 런타임 중에 속성이 변경되면 Compose는 변경사항을 인식

요약

  • 파라미터 : composable의 각 파라미터 Stability를 판단하여 recomposition 중에 skip 해야 하는 composable을 결정
  • Immediate 해결 : composable을 skip 하지 않고 성능 문제가 발생한 경우, var 파라미터와 같은 불안정성의 명확한 원인을 확인해야 함
  • Compiler reports : 컴파일러 보고서를 사용하여 클래스에서 추론되는 Stability를 확인
  • Collections : 항상 collection 클래스(예: List, Set, Map)를 불안정한 것으로 간주. -> 불변성을 보장할 수 없기 때문
  • Other modules : Compose 컴파일러가 실행되지 않는 모듈 -> 항상 불안정하다고 간주
    • 필요한 경우 UI 모델 클래스에서 클래스를 래핑

Stability ~ stability 문제 진단

불필요하거나 과도한 recomposition시 앱의 stability 디버그

Layout Inspector

Layout Inspector를 사용하여 어떤 composable이 recomposition 되는지 확인

Compose가 컴포너트를 recomposition 하거나 skipped count를 표시

Compose 컴파일러 보고서

Compose 컴파일러는 검사를 위해 stability 추론 결과를 출력

설정

컴파일러 보고서는 기본 비활성화로 설정되어 있음

root build.gradle에 스크립트 추가 필요

작업 실행

composable stability을 디버깅 실행

# 정확한 결과를 보장하려면 항상 릴리스 빌드에서 실행
./gradlew assembleRelease -PcomposeCompilerReports=true

JetSnack 의 출력 결과

  • <modulename>-classes.txt : 이 모듈의 클래스 stability 보고서 (샘플)
  • <modulename>-composables.txt : 모듈에서 composable을 얼마나 restartable/skippable에 대한 보고서 (샘플)
  • <modulename>-composables.csv : CSV 버전의 컴포저블 보고서 (샘플)

Composables report

composables.txt 파일은 파라미터의의 stability, restartable, skippable 여부 등 특정 모듈의 composables.txt 함수를 설명

# 완전히 restartable, skippable 하며 stability
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
)

# recomposition 중 skip 불가
# unstable 매개변수인 snacks 때문에 파라미터 변경이 없더라도 skip 하지 않는다
restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  stable index: Int
  unstable snacks: List<Snack>
  stable onSnackClick: Function1<Long, Unit>
  stable modifier: Modifier? = @static Companion
)

Classes report

지정된 모듈의 클래스에 관한 보고서가 포함

// Snack 클래스 정의
data class Snack(
   val id: Long,
   val name: String,
   val imageUrl: String,
   val price: Long,
   val tagline: String = "",
   val tags: Set<String> = emptySet()
)

// Snack 클래스 보고서
// Snack 클래스는 tags 매개변수의 타입(Set<String>)으로 unstable으로 판단
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
}

안정성 ~ stability 문제 해결

strong skipping 활성화

강력 건너뛰기 모드를 사용하면 불안정한 파라미터가 있는 composable을 skip 할 수 있다

  • stability으로 인해 발생하는 성능 문제를 해결하는 가장 쉬운 방법

클래스를 immutable로 만들기

  • Immutable
    • 해당 유형의 인스턴스가 생성된 후에는 속성 값이 절대로 변경될 수 없으며 모든 메서드가 참조 투명인 유형을 나타냅니다.
    • 클래스의 모든 속성이 val, immutable 타입인지 체크
    • String, IntFloat와 같은 Primitive 타입은 항상 변경 불가능
    • 위 항목이 불가능한 경우, mutable 프로퍼티에 Compose state를 사용 (mutableStateOf)
  • Stable
    • mutable 타입
    • Compose 런타임은 타입의 공개 속성이나 메서드 동작이 이전 호출과 다른 결과를 생성하는지 여부와 시점을 인식하지 못함

Immutable collections

Compose Compiler는 List, Map, Set과 같은 collection이 실제로 불변성인지 확신할 수 없으므로 unstable로 표시

해결 방법 : Kotlinx Immutable Collections 사용

  • Set -> ImmutableSet

Stable/Immutable annotation 추가

unstable 클래스에 @Stable/@Immutable annotation 추가

  • Compiler가 Class에 대한 추론을 재정의
  • Compiler 동작을 재정의하면 composable이 예상대로 recomposition 되지 않는 등의 버그 발생 가능
// Immutable으로 annotation 처리
@Immutable
data class Snack(
...
)

collections annotation이 달린 클래스

restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  ...
  unstable snacks: List<Snack>
  ...
)

unstable한 collection 해결 방법

  • stability configuration file 파일에 kotlin.collections.* 추가하여 Kotlin collection을 stable하도록 간주
  • Immutable collection 사용
@Composable
private fun HighlightedSnacks(
   ...
   snacks: ImmutableList<Snack>,
   ...
)
  • 랩핑
@Immutable
data class SnackCollection(
  val snacks: List<Snack>
)

@Composable
private fun HighlightedSnacks(
   index: Int,
   snacks: SnackCollection,
   onSnackClick: (Long) -> Unit,
   modifier: Modifier = Modifier
)
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  stable index: Int
  stable snacks: ImmutableList<Snack>
  stable onSnackClick: Function1<Long, Unit>
  stable modifier: Modifier? = @static Companion
)

위 방법들 중 하나를 선택한 결과

  • skippable + restartable로 표시
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  stable index: Int
  stable snacks: ImmutableList<Snack>
  stable onSnackClick: Function1<Long, Unit>
  stable modifier: Modifier? = @static Companion
)

Stability configuration file

stable 하다고 Compiler에게 알려줄 방법 제공

  • 모듈마다 다른 구성 파일을 제공
  • root에 파일 하나를 만들고 모듈에 전달

Compose Compiler 1.5.5부터 지원

Configuration file 예시

// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider kotlin collections stable
kotlin.collections.*
// Consider my datalayer and all submodules stable
com.datalayer.**
// Consider my generic type stable based off it's first type parameter only
com.example.GenericClass<*,_>

Compose compiler 옵션에 Configuration file path 전달

kotlinOptions {
   freeCompilerArgs += listOf(
      "-P",
      "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
      "${project.absolutePath}/compose_compiler_config.conf"
   )
}

멀티 모듈

클래스가 stable인지 추론 가능

  • non-primitive 타입이 stable하다고 명시적으로 기입
  • Compose compiler로 빌드된 모듈에 있는 경우

해결 방법

아래 중 하나를 선택

  1. Configuration file에 클래스를 추가
  2. data layer 모듈에서 Compose compiler를 사용, @Stable/@Immutable로 지정
    1. data layer에 Compose 종속성을 추가하는 것이 포함
    2. Compose runtime 종속성일 뿐 Compose-UI에 대한 종속성은 아니다
  3. UI 모듈 내에서 data layer 클래스를 UI별 래퍼 클래스로 래핑

Compose compiler를 사용하지 않는 외부 라이브러리를 사용할 때도 동일한 문제가 발생

모든 composable을 skip할 수 있는 것은 아니다

모든 composable을 skippable 타입으로 만들면, 더 많은 문제를 야기하는 초기 최적화가 발생함

skippable하다고 해서 실질적인 이점이 없고 코드를 관리하기 어려운 상황

  • 자주 재구성되지 않거나 전혀 재구성되지 않는 컴포저블
  • 자체적으로 건너뛸 수 있는 컴포저블을 호출하는 컴포저블입니다.
  • 많은 수의 매개변수가 있는 컴포저블은 값비싼 ‘=’ 구현과 같습니다. 이 경우 매개변수가 변경되었는지 확인하는 비용이 저렴한 재구성 비용을 상회할 수 있습니다.

restartable가 보다 높은 오버헤드라고 판단되는 경우 composable에 non-restartable annotation 대응도 가능

Stability ~ Strong skipping mode

Compose compiler에서 사용할 수 있는 모드

  • 파라미터가 unstable composable이 Skip 가능
  • unstable한 캡처가 있는 람다는 remember
  • Compose compiler 1.5.4부터 사용 가능

Strong skipping mode 설정

composeCompiler {
   enableStrongSkipping = true
}

Composable skippability

skip 및 composable 함수에 대해 Compose compiler에서 적용하는 일부 stability 규칙을 완화

Strong skipping mode를 활성화 시

  • restartable한 모든 composable 함수를 Skip하게 됨
    • unstable 파라미터 여부와 관계없이 적용
  • non-restartable composable 함수는 skip 할 수 없는 상태로 유지

Skip 시점

recomposition 중에 composable skip을 결정을 위해 파라미터의 이전 값과 비교

  • 비교 타입은 파라미터의 stability 기준
  • unstable 파라미터 : 인스턴스 동등성 (===)
  • stable 파라미터 : 객체 동등 (Object.equals())

모든 파라미터가 요구사항을 충족하면 Compose는 recomposition 중에 composable을 skip함

restartable하지만 non-skippable한 composable에는 @NonSkippableComposable annotation 사용하면 됨

@NonSkippableComposable
@Composable
fun MyNonSkippableComposable {}

클래스에 stable annotation 추가

인스턴스 동등성(===) 대신 객체 동등성(Object.equals())을 사용을 원하는 경우 클래스에 @Stable annotation 추가

Lambda memoization

Strong skipping mode는 composable 내에서 람다를 더 많이 memoization할 수 있게 함

  • Strong skipping를 활성화하면 composable 함수 내의 모든 람다가 자동으로 remembered 됨
  • Key는 composable 함수와 동일한 비교 규칙을 따름
// 예시 코드
@Composable
fun MyComposable(unstableObject: Unstable, stableObject: Stable) {
   val lambda = {
      use(unstableObject)
      use(stableObject)
   }
}

// Compiler 결과
@Composable
fun MyComposable(unstableObject: Unstable, stableObject: Stable) {
   val lambda = remember(unstableObject, stableObject) {
      {
         use(unstableObject)
         use(stableObject)
      }
   }
}

Memoization와 recomposition

최적화로 인해 recomposition 중에 런타임이 건너뛰는 composable 수가 크게 증가

memoization 방지

@DontMemoize annotation 사용 시 memoize하지 않도록 정의 가능

val lambda = @DontMemoize {
    ...
}

APK 크기

컴파일할 때 skippable composable은 non-skippable composable보다 많은 코드를 생성함

Strong skipping 사용 시, compiler는 거의 모든 composable을 skippable로 표시하고 remember{...}에서 모든 람다를 래핑 함 -> 따라서 strong skipping mode를 사용 설정해도 APK 크기에 미치는 영향은 매우 적다.

도구

Layout Inspector

layout Inspector를 사용하여 레이아웃을 검사하고 Recomposition 횟수를 확인

Recomposition count : https://developer.android.com/develop/ui/compose/tooling/layout-inspector#recomposition-counts

Composition 추적

Composition 추적을 사용하여 system trace에서 Composable 함수를 추적 가능

Best practices

remember를 사용하여 비용이 많이 드는 계산 최소화

Composable 함수는 애니메이션의 모든 프레임만큼 자주 실행될 수 있다.

-> Composable의 본문에서 최대한 적은 계산을 해야 한다.

-> remember를 사용하여 계산 결과를 저장하여 해소. 계산이 한 번 실행되며 필요할 때마다 결과를 가져올 수 있다.

// 잘못된 코드
@Composable
fun ContactList(
   contacts: List<Contact>,
   comparator: Comparator<Contact>,
   modifier: Modifier = Modifier
) {
   LazyColumn(modifier) {
      // ContactsList가 recomposition될 때마다 
      // contacts 리스트가 변경되지 않았더라도 전체 리스트를 다시 정렬
      items(contacts.sortedWith(comparator)) { contact ->
         // ...
      }
   }
}

// 올바른 코드
@Composable
fun ContactList(
   contacts: List<Contact>,
   comparator: Comparator<Contact>,
   modifier: Modifier = Modifier
) {
   // LazyColumn 외부에서 정렬하고 정렬된 목록을 remember를 사용하여 저장
   // ContactList가 처음 composition될 때 목록이 한 번 정렬
   // contacts나 comparator가 변경되면 정렬된 목록이 다시 생성
   // 그 외에는 Composable이 캐시된 정렬된 목록을 계속 사용
   val sortedContacts = remember(contacts, comparator) {
      contacts.sortedWith(comparator)
   }

   LazyColumn(modifier) {
      items(sortedContacts) {
         // ...
      }
   }
}

가능하면 composable 외부로 계산을 모두 이동하는 것이 가장 좋다.

  • ViewModel 혹은 이미 정렬된 목록을 composable에 입력으로 제공

lazy layout 키 사용

Lazy layouts은 항목을 효율적으로 재사용하여 필요한 경우에만 다시 생성하거나 recomposion 한다.

-> recomposion을 위해 lazy layout을 최적화 필요

예시) 수정 시간을 기준으로 정렬된 메모 목록을 표시

// 잘못된 코드
@Composable
fun NotesList(notes: List<Note>) {
   LazyColumn {
      // 가장 최근에 수정되었으므로 목록의 맨 위로 이동하고 다른 모든 메모는 한 스팟 아래로 이동
      // Compose 변경되지 않은 항목이 목록에서 이동된다는 것을 인식하지 못함
      // -> 이전 '항목 2'가 삭제되고 항목 3, 항목 4, 그리고 그 아래까지 새 항목이 만들어졌다고 판단
      // -> 결과적으로 실제로 변경된 항목은 하나뿐이지만 목록의 모든 항목을 Recomposition함
      items(
         items = notes
      ) { note ->
         NoteRow(note)
      }
   }
}

// 올바른 코드
@Composable
fun NotesList(notes: List<Note>) {
   LazyColumn {
      // 안정적인 키를 제공하여 불필요한 Recomposition 방지
      items(
         items = notes,
         key = { note ->
            // note의 안정적이고 고유한 키를 반환
            note.id
         }
      ) { note ->
         NoteRow(note)
      }
   }
}

derivedStateOf를 사용하여 recomposition 제한

상태가 빠르게 변경되면 UI가 필요 이상으로 recomposition될 수 있다

  • 스크롤하면 listState가 계속 변경된다 -> 계속 recomposition 됨
  • derivedStateOf : recomposition을 트리거해야 하는 상태 변경을 Compose에게 알림
// 잘못된 코드
val listState = rememberLazyListState()
LazyColumn(state = listState) {
   // ...
}

val showButton = listState.firstVisibleItemIndex > 0
AnimatedVisibility(visible = showButton) {
   ScrollToTopButton()
}


// 올바른 코드
val listState = rememberLazyListState()
LazyColumn(state = listState) {
   // ...
}

val showButton by remember {
   derivedStateOf {
      listState.firstVisibleItemIndex > 0
   }
}
AnimatedVisibility(visible = showButton) {
   ScrollToTopButton()
}

가능한 한 읽기 최대한 연기

상태 읽기를 연기하면 Compose가 recomposition시 가능한 최소 코드를 다시 실행

Case 1

스크롤 상태가 변경되면 Compose는 가장 가까운 상위 recomposition scope를 무효화

-> SnackDetail composable -> Box는 인라인 함수이므로 recomposition scope가 아님

// 가장 가까운 상위 recomposition scope : SnackDetail composable
@Composable
fun SnackDetail() {
   // ...

   Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
      val scroll = rememberScrollState(0)
      // ...
      Title(snack, scroll.value)
      // ...
   } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scroll: Int) {
   // ...
   val offset = with(LocalDensity.current) { scroll.toDp() }

   Column(
      modifier = Modifier
         .offset(y = offset)
   ) {
      // ...
   }
}

Title에서 끌어올린 상태를 참조할 수 있지만 값은 실제로 필요한 Title 내부에서만 읽는다 -> Box는 recomposition 불필요해짐

// 가장 가까운 상위 recomposition scope : Title composable
@Composable
fun SnackDetail() {
   // ...

   Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
      val scroll = rememberScrollState(0)
      // ...
      Title(snack) { scroll.value }
      // ...
   } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
   // ...
   val offset = with(LocalDensity.current) { scrollProvider().toDp() }
   Column(
      modifier = Modifier
         .offset(y = offset)
   ) {
      // ...
   }
}

오프셋 변경으로 Modifier.offset(x: Dp, y: Dp)에서 람다 버전의 Modifier.offset(offset: Density.() -> IntOffset) Modifier로 변경하면 레이아웃 단계에서 스크롤 상태를 읽음

  • 스크롤 상태가 변경되면 Compose는 Composition 단계를 완전히 건너뛰고 Layout 단계로 이동 가능
  • 자주 변경되는 상태 변수를 Modifier에 전달할 때는 가능하면 람다 버전의 Modifier를 사용해야 함
@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
   // ...
   Column(
      modifier = Modifier
         .offset { IntOffset(x = 0, y = scrollProvider()) }
   ) {
      // ...
   }
}

Case 2

Box의 배경색은 두 색상 간에 빠르게 전환되므로, 이 상태는 매우 자주 변경된다

-> Composable이 background Modifier에서 이 상태를 읽음 -> 색상이 모든 프레임에서 변경되므로 Box는 모든 프레임에서 Recomposition이 됨

// animateColorBetween()이 두 색상을 교환하는 함수라고 가정
val color by animateColorBetween(Color.Cyan, Color.Magenta)

Box(
   Modifier
      .fillMaxSize()
      .background(color)
)

람다 기반 Modifier인 drawBehind 사용으로 해결

-> draw 단계에서만 색상 상태를 읽으므로, composition 및 layout 단계를 완전히 건너뛸 수 있다.

val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(
   Modifier
      .fillMaxSize()
      .drawBehind {
         drawRect(color)
      }
)

역방향 쓰기 방지

Compose의 핵심 가정 : 이미 READ한 상태에서 WRITE하지 않는다

  • 역방향 쓰기라고 하며 모든 프레임에서 recomposition이 끝없이 발생할 수 있다.
@Composable
fun BadComposable() {
   var count by remember { mutableStateOf(0) }

   // 클릭 시 recomposition을 유발
   Button(onClick = { count++ }, Modifier.wrapContentSize()) {
      Text("Recompose")
   }

   Text("$count")
   count++ // 역방향 쓰기, 읽은 뒤 상태 쓰기
}

Composition에서 상태에 write하지 않음으로써 역방향 쓰기를 완전히 방지 가능.

  • 가능하면 onClick과 같이 항상 이벤트에 대한 응답으로, 그리고 람다에서 상태에 write하면 된다

comments powered by Disqus

Currnte Pages Tags

Android AndroidX Compose

About

Pluu, Android Developer Blog Site

이전 블로그 링크 :네이버 블로그

Using Theme : SOLID SOLID Github

Social Links