$30 off During Our Annual Pro Sale. View Details »

Jetpack Compose Structure and Stability

Jaewoong
September 04, 2024

Jetpack Compose Structure and Stability

Jaewoong

September 04, 2024
Tweet

More Decks by Jaewoong

Other Decks in Programming

Transcript

  1. G E T S T R E A M .

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

    I O skydoves @github_skydoves Android Developer Advocate @ Stream Jaewoong Eum
  3. 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
  4. G E T S T R E A M .

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

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

    I O Jetpack Compose Structure Code Transformation In-memory Representation Rendering Layout Trees
  7. 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).
  8. 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).
  9. 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.
  10. 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.
  11. 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.
  12. G E T S T R E A M .

    I O Declarative UI
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. G E T S T R E A M .

    I O Declarative vs. Imperative <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>
  24. G E T S T R E A M .

    I O Declarative vs. Imperative <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> @Composable fun CounterButton(count: Int, onClick: () -> Unit) { Button(onClick = onClick) { Text("Clicked: $count") } }
  25. 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. <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>
  26. 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",
  27. G E T S T R E A M .

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

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

    I O Composable Functions
  30. 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
  31. G E T S T R E A M .

    I O Composable Functions The meaning @Composable fun Place(name: String) { // composable code }
  32. 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
  33. 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.
  34. 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
  35. 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<Place> ) { // work.. } ⇒ compile
  36. 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")
  37. 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.
  38. 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.
  39. G E T S T R E A M .

    I O Stability in Compose
  40. 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.
  41. 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
  42. G E T S T R E A M .

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

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

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

    I O Jetpack Compose Phases
  46. 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.
  47. 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.
  48. G E T S T R E A M .

    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>, ) 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.
  49. 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
  50. 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
  51. 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, )
  52. 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, )
  53. 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<String>, )
  54. 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<String>, ) Unstable data class User( val id: Int, var name: String, ) data class User( val id: Int, val images: List<String>, )
  55. 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
  56. 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
  57. 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.
  58. 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.
  59. 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.
  60. 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
  61. 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.<get-shapes>($composer, 0b0110.circle stable textSize: StyleSize? = @static StyleSize.XL stable textStyle: TextStyle? = @dynamic VideoTheme.<get-typography>($composer, 0b0110.titleM stable contentScale: ContentScale? = @static Companion.Crop stable contentDescription: String? = @static null
  62. G E T S T R E A M .

    I O Inferring Composable Functions Compose Compiler Metrics subprojects { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().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
  63. 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.
  64. 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
  65. 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
  66. 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<String>, )
  67. 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<String>, ) @Immutable public data class User( public val id: String, public val nickname: String, public val profileImages: List<String>, )
  68. 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.
  69. G E T S T R E A M .

    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 }
  70. 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<String>, ) Immutable Stable Stable interface UiState<T  Result<T>> { val value: T? val exception: Throwable? val hasSuccess: Boolean get() = exception == null }
  71. G E T S T R E A M .

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

    I O Stabilize Composable Functions Immutable Collections internal var mutableUserList: MutableList<User> = mutableListOf() public val userList: List<User> = mutableUserList Replace with: - kotlinx.collections.immutable (ImmutableList and ImmutableSet) - guava's immutable collections @Composable fun Profile(images: List<String>) { .. } @Composable fun Profile(images: ImmutableList<String>) { .. }
  73. 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, .. ) }
  74. G E T S T R E A M .

    I O Stabilize Composable Functions Wrapper Class Immutable data class ImmutableUserList( val user: List<User>, val expired: java.time.LocalDateTime, ) Composable fun UserAvatars( modifier: Modifier, userList: ImmutableUserList, ) • The userList parameter is considered stable. • The UserAvatars Composable is skippable.
  75. 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.*
  76. 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
  77. 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
  78. G E T S T R E A M .

    I O Thank you.