Slide 1

Slide 1 text

G E T S T R E A M . I O Jetpack Compose Structure and Stability

Slide 2

Slide 2 text

G E T S T R E A M . I O skydoves @github_skydoves Android Developer Advocate @ Stream Jaewoong Eum

Slide 3

Slide 3 text

Open-Source Projects 12,000,000 library downloads every year 1,500,000,000 end-user devices

Slide 4

Slide 4 text

G E T S T R E A M . I O

Slide 5

Slide 5 text

G E T S T R E A M . I O Index 1. Compose Structure 2. Declarative UI 3. Composable Functions 4. Stability in Compose

Slide 6

Slide 6 text

G E T S T R E A M . I O Jetpack Compose

Slide 7

Slide 7 text

G E T S T R E A M . I O Jetpack Compose

Slide 8

Slide 8 text

G E T S T R E A M . I O Jetpack Compose Structure Code Transformation In-memory Representation Rendering Layout Trees

Slide 9

Slide 9 text

G E T S T R E A M . I O Jetpack Compose Structure Compose Compiler The Compose Compiler stands as a pivotal component within Jetpack Compose, written in Kotlin with targeting on Kotlin Multiplatform. Diverging from conventional annotation processing tools like KAPT and KSP, the Compose compiler plugin directly engages with FIR Frontend Intermediate Representation).

Slide 10

Slide 10 text

G E T S T R E A M . I O Jetpack Compose Structure Compose Compiler The Compose Compiler stands as a pivotal component within Jetpack Compose, written in Kotlin with targeting on Kotlin Multiplatform. Diverging from conventional annotation processing tools like KAPT and KSP, the Compose compiler plugin directly engages with FIR Frontend Intermediate Representation).

Slide 11

Slide 11 text

G E T S T R E A M . I O Jetpack Compose Structure Compose Runtime The Compose Runtime serves as the cornerstone of Compose's model and state management. This library operates by memoizing the state of compositions using a slot table, a concept derived from the gap buffer data structure. Under the hood, this runtime undertakes various crucial tasks.

Slide 12

Slide 12 text

G E T S T R E A M . I O Jetpack Compose Structure Compose UI Compose UI, a vital component of Jetpack Compose, encompasses a suite of UI libraries empowering developers to craft layouts by emitting UI through Composable functions. Compose UI libraries offer a variety of components facilitating the construction of Compose layout trees. These layout trees, once created, are consumed by the Compose Runtime.

Slide 13

Slide 13 text

G E T S T R E A M . I O Jetpack Compose Structure Compose Compiler The Compose Compiler stands as a pivotal component within Jetpack Compose, written in Kotlin with targeting on Kotlin Multiplatform. Diverging from conventional annotation processing tools like KAPT and KSP, the Compose compiler plugin directly engages with FIR Frontend Intermediate Representation). Compose Runtime The Compose Runtime serves as the cornerstone of Compose's model and state management. This library operates by memoizing the state of compositions using a slot table, a concept derived from the gap buffer data structure. Under the hood, this runtime undertakes various crucial tasks. Compose UI Compose UI, a vital component of Jetpack Compose, encompasses a suite of UI libraries empowering developers to craft layouts by emitting UI through Composable functions. Compose UI libraries offer a variety of components facilitating the construction of Compose layout trees. These layout trees, once created, are consumed by the Compose Runtime.

Slide 14

Slide 14 text

G E T S T R E A M . I O Declarative UI

Slide 15

Slide 15 text

G E T S T R E A M . I O Declarative UI 1. Defining components with functions or classes Developers should be able to build applications using components that encompass both essential functionalities and user interface elements. Simultaneously, it's crucial to reduce the language gap between XML and native languages like Java and Kotlin to facilitate seamless component development. Characteristics

Slide 16

Slide 16 text

G E T S T R E A M . I O Declarative UI 1. Defining components with functions or classes Developers should be able to build applications using components that encompass both essential functionalities and user interface elements. Simultaneously, it's crucial to reduce the language gap between XML and native languages like Java and Kotlin to facilitate seamless component development. 2. Managing States for Components In a declarative UI, the framework or library is responsible for managing the state, which encompasses tasks like storing and retrieving data for components. Each component can then be invalidated based on changes in the state. Characteristics

Slide 17

Slide 17 text

G E T S T R E A M . I O Declarative UI 1. Defining components with functions or classes Developers should be able to build applications using components that encompass both essential functionalities and user interface elements. Simultaneously, it's crucial to reduce the language gap between XML and native languages like Java and Kotlin to facilitate seamless component development. 2. Managing States for Components In a declarative UI, the framework or library is responsible for managing the state, which encompasses tasks like storing and retrieving data for components. Each component can then be invalidated based on changes in the state. 3. Binding Data Directly to Components Model data should be bound to the UI at the component level, and this can be accomplished grammatically. Characteristics

Slide 18

Slide 18 text

G E T S T R E A M . I O Declarative UI 1. Defining components with functions or classes Developers should be able to build applications using components that encompass both essential functionalities and user interface elements. Simultaneously, it's crucial to reduce the language gap between XML and native languages like Java and Kotlin to facilitate seamless component development. 2. Managing States for Components In a declarative UI, the framework or library is responsible for managing the state, which encompasses tasks like storing and retrieving data for components. Each component can then be invalidated based on changes in the state. 3. Binding Data Directly to Components Model data should be bound to the UI at the component level, and this can be accomplished grammatically. 4. Ensuring Component Idempotence In declarative programming, ensuring components should be idempotent. This means that regardless of how many times the function has been executed, the result remains the same. This characteristic greatly enhances their reusability. Characteristics

Slide 19

Slide 19 text

G E T S T R E A M . 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") } } Learn by samples

Slide 20

Slide 20 text

G E T S T R E A M . 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") } } interpreted and transformed by Compose Compiler Compile 1. Defining components with functions or classes

Slide 21

Slide 21 text

G E T S T R E A M . I O @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") } } Declarative UI Runtime interpreted and transformed by Compose Compiler In-memory representation in Runtime

Slide 22

Slide 22 text

G E T S T R E A M . I O Declarative UI States are managed by Compose Runtime. Compose Runtime manages the lifecycle of each composition. 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. Managing States for Components

Slide 23

Slide 23 text

G E T S T R E A M . 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") } } Layout Node will be created by Compose UI components Rendering UI

Slide 24

Slide 24 text

G E T S T R E A M . 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. Binding Data Directly to Components 4. Ensuring Component Idempotence

Slide 25

Slide 25 text

G E T S T R E A M . I O Declarative vs. Imperative

Slide 26

Slide 26 text

G E T S T R E A M . I O Declarative vs. Imperative @Composable fun CounterButton(count: Int, onClick: () -> Unit) { Button(onClick = onClick) { Text("Clicked: $count") } }

Slide 27

Slide 27 text

G E T S T R E A M . I O Declarative vs. Imperative var counter = 0 binding.button.setOnClickListener { counter++ binding.button.text = counter.toString() } 1. States should be managed and invalidate UI changes manually. 2. Data cannot be bound directly with the UI declaration.

Slide 28

Slide 28 text

G E T S T R E A M . I O Advantages of Declarative UI 1. Consistency in language across UI and domain Developers should be able to build applications using components that encompass both essential functionalities and user interface elements. Simultaneously, it's crucial to reduce the language gap between XML and native languages like Java and Kotlin to facilitate seamless component development. 2. Automatic management of states and UI invalidation States are managed by Compose Runtime and UI components are invalidated automatically by tracking the states. 3. Enhanced component reusability through idempotence Each component are idempotence from the same given inputs, so it increases the reusability extremely. 4. Direct connection of domain data with UI declaration So developers can focus on "What they want to do" instead of "How they can do",

Slide 29

Slide 29 text

G E T S T R E A M . I O Compose vs. XML @Composable fun ComposeList(items: List) { LazyColumn { items(items) { item -> ListItem(text = item) } } } @Composable fun ListItem(text: String) { // Render a single list item } Compose XML class MyAdapter(private val items: List) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: .. class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { .. recyclerView.layoutManager = LinearLayoutManager(this) recyclerView.adapter = MyAdapter(items)

Slide 30

Slide 30 text

G E T S T R E A M . I O Compose Inspiration

Slide 31

Slide 31 text

G E T S T R E A M . I O Composable Functions

Slide 32

Slide 32 text

G E T S T R E A M . I O Composable Functions The meaning @Composable fun Place(name: String) { // composable code } Layout Node Composable tree

Slide 33

Slide 33 text

G E T S T R E A M . I O Composable Functions The meaning @Composable fun Place(name: String) { // composable code }

Slide 34

Slide 34 text

G E T S T R E A M . I O Composable Functions @Composable fun Place(name: String) { Person(name) } @Composable fun Person(name: String) { // composable code } fun place(name: String) { Person(name) } @Composable fun Person(name: String) { // composable code } ✅ ❌ Calling Context

Slide 35

Slide 35 text

G E T S T R E A M . I O Composable Functions Calling Context ● Compose Compiler transforms the intermediate representation IR) of composable functions. ● Compose adds a new parameter, `$composer`, to all composable functions at the end. ● The `$composer` instance intermediates between Composable functions and Compose Runtime.

Slide 36

Slide 36 text

G E T S T R E A M . I O Composable Functions @Composable fun NamePlate(name: String, lastname: String) { Column(modifier = Modifier.padding(16.dp)) { Text(text = name) Text(text = lastname) } } fun NamePlate( name: String, lastname: String, $composer: Composer<*> ) { ... Column(modifier = Modifier.padding(16.dp), $composer) { Text( text = name, $composer ) Text( text = lastname, $composer ) } ... } Calling Context ⇒ compile

Slide 37

Slide 37 text

G E T S T R E A M . I O Composable Functions vs. suspend function ● Kotlin solves asynchronous or non-blocking programming in a flexible way by providing coroutine support at the language level. ● Suspend functions only can be used inside coroutine scopes or other suspend functions. ● Kotlin compiler generates a `Continuation` type parameter for every suspend function at compile time. suspend fun fetchPlace(name: String): Place { // work.. } fun fetchPlace( name: String, callback: Continuation ) { // work.. } ⇒ compile

Slide 38

Slide 38 text

G E T S T R E A M . I O Composable Functions The function coloring @Composable fun Place(name: String) { Person(name) } @Composable fun Person(name: String) { // composable code } setContent { Place(name = "skydoves") } suspend fun fetchPlace(name: String): Place { getPlaceFromDB(name = name) } suspend fun getPlaceFromDB(name: String): Place { // work.. } coroutineScope.launch { fetchPlace(name = "skydoves")

Slide 39

Slide 39 text

G E T S T R E A M . I O Composable Functions Restartable ● Composable functions can be re-executed, called recomposition, unlike standard functions. ● The recomposition occurs when inputs or states change to keep its in-memory representation always up to date. ● The Compose compiler finds all Composable functions that read some state and teaches the runtime how to restart them.

Slide 40

Slide 40 text

G E T S T R E A M . I O Composable Functions Idempotent #Run1 #Run2 For the same input data, the result will be the same. ● Re-executing a Composable function multiple times with the same input parameters should consistently produce the same UI tree. ● Compose Runtime relies on this assumption Idempotent) for things like recomposition. ● The results of a composable function are already in memory; hence, Compose Runtime doesn't re-execute for the same input by assuming Composable functions are idempotent.

Slide 41

Slide 41 text

G E T S T R E A M . I O Stability in Compose

Slide 42

Slide 42 text

G E T S T R E A M . I O Jetpack Compose Histories 80 Compose Compiler 120 Compose UI & Runtime versions have been released. Jetpack Compose continues to evolve with steady performance enhancements.

Slide 43

Slide 43 text

G E T S T R E A M . I O Jetpack Compose Histories Compose UI Compose Compiler & Runtime ● Smart Recomposition ● Strong Skipping Mode ● File Configuration

Slide 44

Slide 44 text

G E T S T R E A M . I O Jetpack Compose Phases

Slide 45

Slide 45 text

G E T S T R E A M . I O Jetpack Compose Phases

Slide 46

Slide 46 text

G E T S T R E A M . I O Jetpack Compose Phases

Slide 47

Slide 47 text

G E T S T R E A M . I O Jetpack Compose Phases

Slide 48

Slide 48 text

G E T S T R E A M . I O Understanding Stability Recomposition 1. Observing State Changes Jetpack Compose offers an effective mechanism, State, to trigger recomposition by monitoring state changes using the State API provided by the Compose runtime library.

Slide 49

Slide 49 text

G E T S T R E A M . I O Understanding Stability Recomposition 1. Observing State Changes Jetpack Compose offers an effective mechanism, State, to trigger recomposition by monitoring state changes using the State API provided by the Compose runtime library. 2. Input Changes The Compose runtime uses the equals function to detect changes in your arguments for stable parameters. If equals returns false, the runtime interprets this as a change in the input data.

Slide 50

Slide 50 text

G E T S T R E A M . I O Understanding Stability Stable vs. Unstable @Composable fun Profile(user: User, posts: List) { // composable code } compile @Composable fun Profile( stable user: User, unstable posts: List, ) If a Composable function contains at least one unstable parameter, recomposition will always occur. Conversely, if a Composable function contains only stable parameters, recomposition can be skipped, thereby reducing unnecessary work.

Slide 51

Slide 51 text

G E T S T R E A M . I O Understanding Stability Stable vs. Unstable ● Primitive types, including String, are inherently stable. ● Function types, represented by lambda expressions like Int → String, are considered stable. ● Classes, particularly data classes characterized by immutable, stable public properties or those explicitly marked as stable by using the stability annotations, such as Stable, or Immutable, are considered stable. Stable

Slide 52

Slide 52 text

G E T S T R E A M . I O Understanding Stability Stable vs. Unstable ● Interfaces, including List, Map, and others, along with abstract classes like the Any type that are not predictable of implementation on compile time, are considered unstable. ● Classes, especially data classes containing at least one mutable or inherently unstable public property, will be categorized as unstable. Unstable

Slide 53

Slide 53 text

G E T S T R E A M . I O Understanding Stability Stable vs. Unstable Stable data class User( val id: Int, val name: String, )

Slide 54

Slide 54 text

G E T S T R E A M . 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, )

Slide 55

Slide 55 text

G E T S T R E A M . 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, )

Slide 56

Slide 56 text

G E T S T R E A M . 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, ) Unstable data class User( val id: Int, var name: String, ) data class User( val id: Int, val images: List, )

Slide 57

Slide 57 text

G E T S T R E A M . I O Understanding Stability Smart Recomposition #Run1 #Run2 Skip recomposition → Smart Recomposition ❌ The data is same (equals()) and stable

Slide 58

Slide 58 text

G E T S T R E A M . I O Understanding Stability Smart Recomposition Skip recomposition → Smart Recomposition ❌ The data is same (equals()) and stable Recomposition Recomposition Recomposition Skip Recomposition

Slide 59

Slide 59 text

G E T S T R E A M . I O Understanding Stability Smart Recomposition 1. Decision-Based on Stability - If a parameter is stable and its value hasnʼt changed (equals() returns true), Compose skips recomposing the related UI components. - If a parameter is unstable or if it is stable but its value has changed (equals() returns false), the runtime initiates recomposition to invalidate and redraw the UI layouts. 2. Equality Check Whenever a new stable input type is passed to a Composable function, it is invariably compared with its predecessor using the classʼs equals() method.

Slide 60

Slide 60 text

G E T S T R E A M . I O Inferring Composable Functions ● Restartable ● Skippable ● Movable ● .. @Composable fun NamePlate(name: String, lastname: String) { Column(modifier = Modifier.padding(16.dp)) { Text(text = name) Text(text = lastname) } } Compile The Compose Compiler infers characteristics of Composable functions at compile time, such as Restartable, Skippable, and Movable.

Slide 61

Slide 61 text

G E T S T R E A M . I O Inferring Composable Functions Restartable #Run1 #Run2 If the inputs are different, Composable should be re-executed with the new ones. Most Composable functions are restartable and idempotent.

Slide 62

Slide 62 text

G E T S T R E A M . I O Inferring Composable Functions Skippable ❌ #Run1 #Run2 If a Composable function consists only of stable parameters, it is considered skippable. Skip recomposition → Smart Recomposition The data is same (equals()) and stable

Slide 63

Slide 63 text

G E T S T R E A M . I O Inferring Composable Functions Compose Compiler Metrics stable class StreamShapes { stable val circle: Shape stable val square: Shape stable val button: Shape stable val input: Shape stable val dialog: Shape stable val sheet: Shape stable val indicator: Shape stable val container: Shape } restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun Avatar( stable modifier: Modifier? = @static Companion stable imageUrl: String? = @static null stable initials: String? = @static null stable shape: Shape? = @dynamic VideoTheme.($composer, 0b0110.circle stable textSize: StyleSize? = @static StyleSize.XL stable textStyle: TextStyle? = @dynamic VideoTheme.($composer, 0b0110.titleM stable contentScale: ContentScale? = @static Companion.Crop stable contentDescription: String? = @static null

Slide 64

Slide 64 text

G E T S T R E A M . I O Inferring Composable Functions Compose Compiler Metrics subprojects { tasks.withType().all { kotlinOptions.freeCompilerArgs += listOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + project.buildDir.absolutePath + "/compose_metrics" ) kotlinOptions.freeCompilerArgs += listOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + project.buildDir.absolutePath + "/compose_metrics" ) } } 💻 bit.ly/3V2Q1wB

Slide 65

Slide 65 text

G E T S T R E A M . I O Stability Annotations Compose Runtime Immutable Stable The Compose Runtime offers two stability annotations: Immutable and Stable. These annotations can be used to ensure that specific classes or interfaces are considered stable.

Slide 66

Slide 66 text

G E T S T R E A M . I O Stability Annotations The Immutable annotation serves as a robust commitment to the Compose compiler, ensuring that all public properties and fields of the class will never be changed(immutable) after their initial creation. There are two rules that you should keep in mind before using this annotation: 1. Use the val keyword for all public properties to ensure they are immutable. 2. Avoid custom setters and ensure public properties do not support mutability. Immutable

Slide 67

Slide 67 text

G E T S T R E A M . I O Stability Annotations Immutable public data class User( public val id: String, public val nickname: String, public val profileImage: String, ) Stable

Slide 68

Slide 68 text

G E T S T R E A M . 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, )

Slide 69

Slide 69 text

G E T S T R E A M . 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, ) @Immutable public data class User( public val id: String, public val nickname: String, public val profileImages: List, )

Slide 70

Slide 70 text

G E T S T R E A M . I O Stability Annotations Stable The Stable annotation represents a strong but slightly less stringent commitment to the Compose compiler compared to the Immutable annotation. When applied to a function or a property, the Stable annotation signifies that a type may be mutable. The term "Stable" in this context implies that the function will consistently return the same result for the same inputs, ensuring predictable behavior despite potential mutability. Therefore, the Stable annotation is most suitable for classes whose public properties are immutable, yet the class itself may not qualify as stable.

Slide 71

Slide 71 text

G E T S T R E A M . I O Stability Annotations Stable Stable interface State { val value: T } Stable interface MutableState : State { override var value: T operator fun component1(): T operator fun component2(): T) → Unit }

Slide 72

Slide 72 text

G E T S T R E A M . I O Stability Annotations Immutable vs. Stable Immutable public data class User( public val id: String, public val nickname: String, public val profileImages: List, ) Immutable Stable Stable interface UiState> { val value: T? val exception: Throwable? val hasSuccess: Boolean get() = exception == null }

Slide 73

Slide 73 text

G E T S T R E A M . I O Stabilize Composable Functions Immutable Collections internal var mutableUserList: MutableList = mutableListOf() public val userList: List = mutableUserList @Composable fun Profile(images: List) { .. }

Slide 74

Slide 74 text

G E T S T R E A M . I O Stabilize Composable Functions Immutable Collections internal var mutableUserList: MutableList = mutableListOf() public val userList: List = mutableUserList Replace with: - kotlinx.collections.immutable (ImmutableList and ImmutableSet) - guava's immutable collections @Composable fun Profile(images: List) { .. } @Composable fun Profile(images: ImmutableList) { .. }

Slide 75

Slide 75 text

G E T S T R E A M . 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, .. ) }

Slide 76

Slide 76 text

G E T S T R E A M . I O Stabilize Composable Functions Wrapper Class Immutable data class ImmutableUserList( val user: List, val expired: java.time.LocalDateTime, ) Composable fun UserAvatars( modifier: Modifier, userList: ImmutableUserList, ) ● The userList parameter is considered stable. ● The UserAvatars Composable is skippable.

Slide 77

Slide 77 text

G E T S T R E A M . I O Stabilize Composable Functions File Configuration 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.*

Slide 78

Slide 78 text

G E T S T R E A M . I O Blog Post Optimize App Performance By Mastering Stability in Jetpack Compose https://medium.com/proandroiddev/optimize-app-performance- by-mastering-stability-in-jetpack-compose-69f40a8c785d

Slide 79

Slide 79 text

G E T S T R E A M . I O https://github.com/skydoves [email protected] https://twitter.com/github_skydoves https://medium.com/@skydoves Contact

Slide 80

Slide 80 text

G E T S T R E A M . I O Thank you.