I O Jetpack Compose Structure Compose Compiler • Jetpack Compose 내에서 중추적인 역할을 하는 핵심 구성 요소 • 순수한 Kotlin으로 작성된 Kotlin 컴파일러 플러그인 • KAPT나 KSP와 같은 annotation processor와는 달리, Compose 컴파일러는 FIR Frontend Intermediate Representation)을 통하여 개발자가 작성한 코드를 정적 분석 및 변형
I O Jetpack Compose Structure Compose Runtime • Compose 모델 및 상태 관리의 초석 역할 • Gap Buffer라는 데이터 구조에서 파생된 Slot Table을 사용하여 상태 저장 등 메모리 운용 • Composable 함수에 대한 실질적인 생명주기 관리 및 UI에 대한 정보를 담고 있는 메모리 표현
I O Jetpack Compose Structure Compose UI • 개발자가 Composable 함수를 통해 레이아웃을 생성할 수 있도록 여러 UI 라이브러리를 통해 컴포넌트 제공 • 각 컴포넌트는 LayoutNode를 생성하고, 결과적으로 Compose 레이아웃 트리 구성을 용이하게 함 • Compose Runtime에 의하여 소비되는 것이 기본 원칙
I O Jetpack Compose Structure Compose Compiler 컴포즈 컴파일러는 Jetpack Compose의 핵심 구성 요소로, Kotlin으로 작성되었으며 Kotlin Multiplatform을 대상으로 함. KAPT와 KSP 같은 전통적인 어노테이션 처리 도구와 달리, 컴포즈 컴파일러 플러그인은 FIRFrontend Intermediate Representation)을 직접 다룸. Compose Runtime 컴포즈 런타임은 컴포즈의 모델 및 상태 관리를 위한 중추적인 역할을 수행. SlotTable이라는 자료구조를 사용해 Composable 함수에 대한 데이터를 관리하고, State(상태)를 관리하여 Composable 함수를 재실행 시키는 등 런타임 동작에 관여. Compose UI 컴포즈 UI는 개발자가 Composable 함수로 UI를 생성하여 레이아웃을 구성할 수 있도록 하는 UI 컴포넌트들의 집합체. 컴포즈 UI는 Compose의 레이아웃 트리를 구축하는 데 필요한 다양한 구성 요소를 제공하며, 이 레이아웃 트리는 컴포즈 런타임에 의해 사용됨.
I O Declarative UI 1. 함수나 클래스로 구성 요소 정의 개발자는 필수 기능(비즈니스 함수 호출 등)과 사용자 인터페이스 요소(레이아웃)를 모두 포함하는 컴포넌트를 사용하여 애플리케이션을 개발할 수 있어야 한다. 동시에, XML과 Java, Kotlin과 같은 레이아웃 정의 부분과 도메인 구현 간의 언어적 격차를 줄여 원활한 구성 요소 개발을 촉진할 수 있어야 한다. Characteristics
I O Declarative UI 1. 함수나 클래스로 구성 요소 정의 개발자는 필수 기능(비즈니스 함수 호출 등)과 사용자 인터페이스 요소(레이아웃)를 모두 포함하는 컴포넌트를 사용하여 애플리케이션을 개발할 수 있어야 한다. 동시에, XML과 Java, Kotlin과 같은 레이아웃 정의 부분과 도메인 구현 간의 언어적 격차를 줄여 원활한 구성 요소 개발을 촉진할 수 있어야 한다. 2. 구성 요소의 상태 관리 선언적 UI에서는 프레임워크나 라이브러리가 상태를 관리하며, 여기에는 구성 요소의 데이터를 저장하고 검색하는 작업이 포함된다. 각 구성 요소는 상태 변화에 따라 개발자에 의해서가 아니라 프레임워크에 의해서 업데이트되어야 한다. Characteristics
I O Declarative UI 1. 함수나 클래스로 구성 요소 정의 개발자는 필수 기능(비즈니스 함수 호출 등)과 사용자 인터페이스 요소(레이아웃)를 모두 포함하는 컴포넌트를 사용하여 애플리케이션을 개발할 수 있어야 한다. 동시에, XML과 Java, Kotlin과 같은 레이아웃 정의 부분과 도메인 구현 간의 언어적 격차를 줄여 원활한 구성 요소 개발을 촉진할 수 있어야 한다. 2. 구성 요소의 상태 관리 선언적 UI에서는 프레임워크나 라이브러리가 상태를 관리하며, 여기에는 구성 요소의 데이터를 저장하고 검색하는 작업이 포함된다. 각 구성 요소는 상태 변화에 따라 개발자에 의해서가 아니라 프레임워크에 의해서 업데이트되어야 한다. 3. 데이터를 구성 요소에 직접 바인딩 모델 데이터를 컴포넌트 수준에서 UI에 직접적으로 바인딩해야 하며, 이는 레이아웃을 구현하는 데 사용된 언어와 동일한 언어와 레벨에서 온전하게 처리할 수 있어야 한다. Characteristics
I O Declarative UI 1. 함수나 클래스로 구성 요소 정의 개발자는 필수 기능(비즈니스 함수 호출 등)과 사용자 인터페이스 요소(레이아웃)를 모두 포함하는 컴포넌트를 사용하여 애플리케이션을 개발할 수 있어야 한다. 동시에, XML과 Java, Kotlin과 같은 레이아웃 정의 부분과 도메인 구현 간의 언어적 격차를 줄여 원활한 구성 요소 개발을 촉진할 수 있어야 한다. 2. 구성 요소의 상태 관리 선언적 UI에서는 프레임워크나 라이브러리가 상태를 관리하며, 여기에는 구성 요소의 데이터를 저장하고 검색하는 작업이 포함된다. 각 구성 요소는 상태 변화에 따라 개발자에 의해서가 아니라 프레임워크에 의해서 업데이트되어야 한다. 3. 데이터를 구성 요소에 직접 바인딩 모델 데이터를 컴포넌트 수준에서 UI에 직접적으로 바인딩해야 하며, 이는 레이아웃을 구현하는 데 사용된 언어와 동일한 언어와 레벨에서 온전하게 처리할 수 있어야 한다. 4. 구성 요소의 멱등성 보장 선언적 프로그래밍에서는 구성 요소가 멱등성을 가져야 한다. 즉, 함수가 몇 번 실행되었는지에 관계없이 동일한 인풋에 대해서 결과는 항상 동일해야 한다. 이를 통해 컴포넌트의 재사용성을 크게 향상시킨다. Characteristics
I O Declarative UI @Composable fun Main() { var count by remember { mutableStateOf(0) } CounterButton(count) { count++ } } @Composable fun CounterButton(count: Int, onClick: () -> Unit) { Button(onClick = onClick) { Text("Clicked: $count") } } Compose Compiler에 의해 해석 및 변환됨 Compile 1. 함수나 클래스로 구성 요소 정의
I O Declarative UI 1. 상태는 Compose Runtime에서 관리됨 2. Compose Runtime은 각 컴포넌트의 라이프사이클을 관리. Runtime @Composable fun Main() { var count by remember { mutableStateOf(0) } CounterButton(count) { count++ } } @Composable fun CounterButton(count: Int, onClick: () -> Unit) { Button(onClick = onClick) { Text("Clicked: $count") } } 2. 구성 요소의 상태 관리
I O Declarative UI @Composable fun Main() { var count by remember { mutableStateOf(0) } CounterButton(count) { count++ } } @Composable fun CounterButton(count: Int, onClick: () -> Unit) { Button(onClick = onClick) { Text("Clicked: $count") } } Rendering UI 3. 데이터를 구성 요소에 직접 바인딩 4. 구성 요소의 멱등성 보장
I O Declarative vs. Imperative var counter = 0 binding.button.setOnClickListener { counter++ binding.button.text = counter.toString() } 1. 상태는 관리되어야 하며 UI 변경 사항은 수동으로 무효화되어야 함. 2. 데이터는 UI 선언과 직접 바인딩될 수 없음. <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="horizontal" android:padding="4dp"> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="Clicked: 0" /> </RelativeLayout>
I O Advantages of Declarative UI 1. UI와 도메인 간 언어의 일관성 개발자는 필수 기능과 사용자 인터페이스 요소를 모두 포함하는 컴포넌트를 사용하여 애플리케이션을 구축할 수 있다. 동시에, XML과 Java, Kotlin과 같은 네이티브 언어 간의 언어적 격차를 줄여 원활한 구성 요소 개발을 촉진할 수 있다. 2. 상태 및 UI 무효화의 자동 관리 상태는 Compose Runtime에 의해 관리되며, UI 구성 요소는 상태를 추적하여 자동으로 업데이트된다. 3. 멱등성을 통한 구성 요소 재사용성 강화 각 구성 요소는 동일한 입력값에 대해 멱등성을 가지므로, 재사용성이 극대화된다. 4. 도메인 데이터와 UI 선언의 직접적인 연결 이를 통해 개발자는 "어떻게 할 것인가"보다는 "무엇을 할 것인가"에 집중할 수 있다.
I O Compose vs. XML @Composable fun ComposeList(items: List<String>) { LazyColumn { items(items) { item -> ListItem(text = item) } } } @Composable fun ListItem(text: String) { // Render a single list item } Compose XML <ListView android:id="@+id/listView" android:layout_width="match_parent" android:layout_height="match_parent" /> class MyAdapter(private val items: List<String>) : RecyclerView.Adapter<MyAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: .. class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { .. recyclerView.layoutManager = LinearLayoutManager(this) recyclerView.adapter = MyAdapter(items)
I O Composable Functions Calling Context • 컴포즈 컴파일러는 Composable 함수의 중간 표현IR, Intermediate Representation)을 변환 • 모든 Composable 함수의 마지막에 `$composer`라는 새로운 매개변수를 추가 • `$composer` 인스턴스는 Composable 함수와 컴포즈 런타임 사이에서 중재 역할을 담당
I O Composable Functions vs. suspend function • Kotlin은 언어 수준에서 코루틴 지원을 제공하여 비동기 또는 논블로킹 프로그래밍을 유연하게 해결 • Suspend 함수는 코루틴 스코프나 다른 suspend 함수 내에서만 사용할 수 있음 • Kotlin 컴파일러는 컴파일 시 각 suspend 함수에 대해 Continuation 타입의 매개변수를 생성 suspend fun fetchPlace(name: String): Place { // work.. } fun fetchPlace( name: String, callback: Continuation<Place> ) { // work.. } ⇒ compile
I O Composable Functions Restartable • Composable 함수는 일반 함수와 달리 다시 실행될 수 있는데, 이를 재구성(recomposition)이라고 부름 • Recomposition은 입력값이나 상태가 변경될 때 발생하며, 이를 통해 메모리 내의 표현을 항상 최신 상태로 유지 • 컴포즈 컴파일러는 상태를 읽는 모든 Composable 함수를 찾아 런타임이 이들을 어떻게 다시 시작할지 알려줌
I O Composable Functions Idempotent #Run1 #Run2 동일한 입력값에 대해서 동일한 결과 출력 • 동일한 입력 매개변수로 컴포저블 함수를 여러 번 다시 실행하더라도 일관된 UI 트리를 생성해야함 • 컴포즈 런타임은 이러한 가정(멱등성, Idempotent)에 의존하여 Recomposition과 같은 작업을 수행합니다. • Composable 함수의 결과는 이미 메모리에 저장되어 있으므로, 컴포즈 런타임은 Composable 함수가 멱등성을 갖는다고 가정하여 동일한 입력에 대해 다시 실행하지 않음
I O Jetpack Compose Histories 80 Compose Compiler 125 Compose UI & Runtime versions have been released. Jetpack Compose continues to evolve with steady performance enhancements.
I O Understanding Stability • Composable 함수는 표준 함수와는 달리 recomposition을 통해 재실행이 가능함 • Recomposition은 메모리 내 표현을 항상 최신 상태로 유지하기 위해 입력값의 변경이나 관찰 중인 State에 변경이 발생할 때 일어남 • Compose 컴파일러는 상태를 관찰하는 모든 Composable 함수를 찾아서 Compose 런타임에게 정보를 전달함 Restartable
I O Understanding Stability Recomposition 1. 관찰 중인 State의 변경 Compose 런타임은 상태 변경을 관찰하고 recomposition을 트리거하는 State라는 효율적인 메커니즘을 제공 var text by remember { mutableIntStateOf(0) } Text( modifier = Modifier.clickable { text++ }, text = "Clicked $text times" )
I O Understanding Stability Recomposition 1. 관찰 중인 State의 변경 Compose 런타임은 상태 변경을 관찰하고 recomposition을 트리거하는 State라는 효율적인 메커니즘을 제공 2. Composable 함수의 매개변수 변경 Compose 런타임은 equals 함수를 사용하여 stable한 매개변수에 대해 입력값의 변경 사항을 감지함. 만약 equals 함수가 false를 반환하면 런타임은 이를 입력 데이터의 변경으로 해석 @Composable fun UserProfile(name: String, image: String) { .. }
I O Understanding Stability Stable vs. Unstable @Composable fun Profile(user: User, posts: List<Post>) { // composable code } compile @Composable fun Profile( stable user: User, unstable posts: List<Post>, ) • Compose 컴파일러는 Composable 함수에 사용된 매개변수에게 stable 혹은 unstable 타입을 부여 • Composable 함수에 unstable한 매개변수가 하나 이상 포함되어 있으면 recomposition이 항상 발생 • Composable 함수가 모두 stable한 매개변수로 이루어져 있다면 recomposition을 건너뛰고 불필요한 작업 생략
I O Understanding Stability Stable vs. Unstable Stable로 간주되는 유형 • 원시 타입 Primitive types) • Int → String 와 같은 람다식으로 표현되는 함수의 유형 (외부값을 캡처하는 경우는 값이 stable인 경우만) • (data) class의 public 프로퍼티가 모두 불변이거나 stable한 경우 • (data) class에 Stable 및 Immutable 어노테이션을 사용하여 명시적으로 stable 하다고 표기된 경우
I O Understanding Stability Stable vs. Unstable Unstable로 간주되는 유형 • 컴파일 타임에 구현체를 예측할 수 없는 Any 유형과 같은 추상 클래스와 List, Map 등을 포함한 모든 인터페이스 • (data) class의 public 프로퍼티 중 최소 하나 이상이 가변적이거나 unstable한 경우
I O Understanding Stability Stable vs. Unstable Stable data class User( val id: Int, val name: String, ) Unstable data class User( val id: Int, var name: String, )
I O Understanding Stability Stable vs. Unstable Stable data class User( val id: Int, val name: String, ) Unstable data class User( val id: Int, var name: String, ) data class User( val id: Int, val images: List<String>, )
I O Understanding Stability Stable vs. Unstable Stable data class User( val id: Int, val name: String, ) @Immutable data class User( val id: Int, val images: List<String>, ) Unstable data class User( val id: Int, var name: String, ) data class User( val id: Int, val images: List<String>, )
I O Understanding Stability Smart Recomposition 1. Stability에 기반한 의사결정 - 매개변수가 stable하고 해당 값이 변경되지 않은 경우 (equals() returns true), Compose는 관련 UI 컴포넌트에 대한 recomposition 작업을 생략 - 매개변수가 unstable하거나 stable하지만 값이 변경 된 경우 (equals() returns false), Compose 런타임은 recomposition을 수행하고 UI 레이아웃을 invalidate하여 다시 랜더링 2. Equality 체크 새로운 stable한 유형의 매개변수가 Composable 함수로 전달되면, equals() 메서드를 사용하여 이전의 매개변수 값과 equality(동등성) 확인을 수행
I O Inferring Composable Functions Restartable #Run1 #Run2 입력값이 달라지면, Composable 함수를 새로운 입력값으로 재실행 대부분의 Composable 함수는 Restartable(재실행 가능)하고 멱등성의 성질을 가짐
I O Inferring Composable Functions Skippable ❌ #Run1 #Run2 Composable 함수가 stable한 매개변수만으로 구성된 경우, recomposition을 생략할 수 있는 Skippable로 분류 recomposition 생략 Smart Recomposition) stable한 데이터 타입의 equals()가 true
I O Stability Annotations Compose Runtime Immutable Stable Compose 런타임은 Immutable과 Stable 이라는 두 가지 stability 어노테이션을 제공하여 특정 클래스나 인터페이스가 stable한 것으로 간주될 수 있도록 할 수 있음
I O Stability Annotations Immutable @Immutable 어노테이션은 Compose 컴파일러에 대한 강력한 약속이며, class의 모든 public 프로퍼티와 필드가 초기화 된 이후에 절대 변경되지 않도록(불변) 보장 @Immutable 어노테이션 사용에 대한 두 가지 규칙: 1. 모든 public 프로퍼티에 대하여 val keyword를 사용하여 불변임을 확인 2. 커스텀 setter 사용을 피하고, 모든 public 프로퍼티에 가변성이 없는지 확인
I O Stability Annotations Immutable public data class User( public val id: String, public val nickname: String, public val profileImage: String, ) Stable
I O Stability Annotations Immutable public data class User( public val id: String, public val nickname: String, public val profileImage: String, ) Stable Unstable public data class User( public val id: String, public val nickname: String, public val profileImages: List<String>, )
I O Stability Annotations Immutable public data class User( public val id: String, public val nickname: String, public val profileImage: String, ) Stable Unstable public data class User( public val id: String, public val nickname: String, public val profileImages: List<String>, ) @Immutable public data class User( public val id: String, public val nickname: String, public val profileImages: List<String>, )
I O Stability Annotations Stable • Stable 어노테이션 또한 Compose 컴파일러에 대한 강력한 약속이지만, Immutable 보다는 조금 느슨한 약속을 의미 • 문맥적으로 "Stable(안정적)"이라는 용어는 함수가 동일한 입력값에 대해 일관되게 동일한 결과를 반환하여, 잠재적인 변경 가능성에도 불구하고 예측 가능한 동작을 보장한다는 것을 의미 • Stable 어노테이션은 public 프로퍼티가 불변인 클래스 혹은 인터페이스에 가장 적합하지만, 클래스 자체나 인터페이스의 구현체가 안정적이지 않을 수 있는 경우에 사용됨
I O Stability Annotations Stable Stable interface State<out T> { val value: T } Stable interface MutableState<T> : State<T> { override var value: T operator fun component1(): T operator fun component2(): T) → Unit }
I O Stability Annotations Immutable vs. Stable Immutable public data class User( public val id: String, public val nickname: String, public val profileImages: List<String>, ) Immutable Stable Stable interface UiState<T Result<T>> { val value: T? val exception: Throwable? val hasSuccess: Boolean get() = exception == null }
I O Composable NonRestartableComposable fun LaunchedEffect( key1 Any?, block: suspend CoroutineScope.() → Unit ) { .. } @NonRestartableComposable • Composable 함수에 사용할 경우 Restartable 속성을 부여하지 않고 recomposition을 생략 • Recomposition의 영향을 받지 않아야 하는 경우 사용하기에 적합 • e.g, Composable 함수 내 상태 관리 및 사이드 이펙트 처리 및 표준 함수만을 호출하는 사례 Stability Annotations NonRestartableComposable
I O Stabilize Composable Functions Immutable Collections internal var mutableUserList: MutableList<User> = mutableListOf() public val userList: List<User> = mutableUserList @Composable fun Profile(images: List<String>) { .. } Unstable
I O Stabilize Composable Functions Immutable Collections Compose Compiler: KnownStableConstructs.kt object KnownStableConstructs { val stableTypes = mapOf( // Guava "com.google.common.collect.ImmutableList" to 0b1, "com.google.common.collect.ImmutableSet" to 0b1, .. // Kotlinx immutable "kotlinx.collections.immutable.ImmutableCollection" to 0b1, "kotlinx.collections.immutable.ImmutableList" to 0b1, .. ) }
I O Stabilize Composable Functions Wrapper Class Immutable data class ImmutableUserList( val user: List<User>, val expired: java.time.LocalDateTime, ) Composable fun UserAvatars( stable modifier: Modifier, stable userList: ImmutableUserList, ) Composable fun UserAvatars( stable modifier: Modifier, unstable user: List<User>, unstable expired: java.time.LocalDateTime, ) Non-Skippable Skippable
I O Stabilize Composable Functions Stability Configuration File kotlinOptions { freeCompilerArgs += listOf( "P", "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" + "${project.absolutePath}/compose_compiler_config.conf" ) } compose_compiler_config.conf // 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<*,_> // Consider our data models stable since we always use immutable classes com.google.samples.apps.nowinandroid.core.model.data.*