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).
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).
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.
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.
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.
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
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
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
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
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
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
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>
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",
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 • 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.
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
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.
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.
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.
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.
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.
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.
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
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
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 Skip recomposition -> Smart Recomposition ❌ The data is same (equals()) and stable Recomposition Recomposition Recomposition Skip Recomposition
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.
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.
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.
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
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.
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
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 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.
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 Stabilize Composable Functions Immutable Collections internal var mutableUserList: MutableList<User> = mutableListOf() public val userList: List<User> = mutableUserList @Composable fun Profile(images: List<String>) { .. }
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>) { .. }
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( modifier: Modifier, userList: ImmutableUserList, ) • The userList parameter is considered stable. • The UserAvatars Composable is skippable.
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.*
I O Kotlin Multiplatform Kotlin Multiplatform (KMP) is a recommended way to share business logic across your apps. This year at Google I/O, Google announced that Android is supporting Kotlin Multiplatform to share business logic across mobile, desktop, web, and server.
I O Compose Multiplatform Declarative framework for sharing UIs across multiple platforms. Based on Kotlin and Jetpack Compose. • Android: Stable • iOS: Alpha • Desktop (Windows, MacOS, Linux): Stable • Web (Wasm): Experimental