Jetpack Compose internals (Jorge Castillo Andrei Shikov)(Z-Library)
,
Programming Language
No Description
8
Views
Registered users can read the full content for free
Register as a Gaohf Library member to read the complete e-book online for free and enjoy a better reading experience.
Page
1
(This page has no text content)
Page
2
Jetpack Compose internals Jorge Castillo This book is for sale at http://leanpub.com/composeinternals This version was published on 2021-07-28 This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and many iterations to get reader feedback, pivot until you have the right book and build traction once you do. © 2021 Jorge Castillo
Page
3
Tweet This Book! Please help Jorge Castillo by spreading the word about this book on Twitter! The suggested hashtag for this book is #composeinternals. Find out what other people are saying about the book by clicking on this link to search for this hashtag on Twitter: #composeinternals
Page
4
This book is dedicated to all the people that helped along the way. I want to say thank you to Manuel Vivo, Joe Birch, Antonio Leiva, Enrique López-Mañas, Andrei Shikov, and Leland Richardson for reviewing parts of this book and providing very valuable feedback. I don’t want to forget about Adam Powell either. Thanks Adam for answering more than a zillion questions about the compiler and the runtime. I also want to have a word for all the incredibly experienced developers that stop by the Jetpack Compose channels from both Kotlinlang and the Android Study Group to discuss about the library. I keep learning a lot from all of you and you have helped to solve lots of doubts. Of course, I want to give special thanks to the Google Jetpack Compose team for the incredible work they are doing with the library, since I am convinced that it is going to be a game changer. Also for embracing the book in such a nice way and even sending interesting proposals. Let’s keep the hype up for a while :) Special thanks and shout-out to Andrei Shikov, who wrote one of the chapters (probably the most interesting one!). Thanks Andrei for making such a big effort to get your content ready for the pre- release. I really appreciate your help, and I must say I have learned a ton from you. Finally, I really want to thank my wife, María Isabel, and my beloved baby, Julia, for giving me all the strength and motivation to face such a complicated challenge during these specially hard times we are all living. Having a family like the one I have is like having a superpower. I am so lucky to have you. I love you both.
Page
5
Contents Prelude . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 Why to read this book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 What this book is not about . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 What this book is about . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 Keep the sources close . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Code snippets and examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1. Composable functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 The nature of Composable functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Composable function properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Calling context . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Idempotent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 Free of side effects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Restartable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Fast execution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Positional memoization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Similarities with suspend functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 Composable functions are colored . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 Composable function types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 2. The Compose compiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 A Kotlin compiler plugin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 Compose annotations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 Registering Compiler extensions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 Static analysis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 Kotlin Compiler version . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 Static Checkers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 Call checks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 Type checks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 Declaration checks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 Diagnostic suppression . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 Runtime version check . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 Code generation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 The Kotlin IR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
Page
6
CONTENTS Lowering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 Inferring class stability . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 Enabling live literals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 Compose lambda memoization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 Injecting the Composer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 Comparison propagation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 Default parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 Control flow group generation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 Klib and decoy generation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 3. The Compose runtime . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 The slot table and the list of changes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 The slot table in depth . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 The list of changes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 The Composer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 Feeding the Composer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 Modeling the Changes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 Optimizing when to write . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 Writing and reading groups . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 Remembering values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 Recompose scopes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 SideEffects in the Composer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 Storing CompositionLocals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 Storing source information . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 Linking Compositions via CompositionContext . . . . . . . . . . . . . . . . . . . . . . . . . . 62 Accessing the current State snapshot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 Navigating the nodes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 Keeping reader and writer in sync . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 Applying the changes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 Performance when building the node tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 How changes are applied . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 Attaching and drawing the nodes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 Composition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 Creating a Composition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 The initial Composition process . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 Applying changes after initial Composition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 Additional information about the Composition . . . . . . . . . . . . . . . . . . . . . . . . . . 74 The Recomposer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 Spawning the Recomposer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 Recomposition process . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 Concurrent recomposition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 Recomposer states . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
Page
7
CONTENTS 4. Compose UI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 5. State snapshot system . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 6. Smart Recomposition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 7. Effects and effect handlers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 Introducing side effects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 Side effects in Compose . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 What we need . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 Effect Handlers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 Non suspended effects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 Suspended effects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 Third party library adapters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 8. The Composable lifecycle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 9. Advanced Compose Runtime use cases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 Compose runtime vs Compose UI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 Composition of vector graphics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98 Building vector image tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 Integrating vector composition into Compose UI . . . . . . . . . . . . . . . . . . . . . . . . . 104 Managing DOM with Compose . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 Standalone composition in the browser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
Page
8
Prelude Why to read this book Jetpack Compose will become the “de facto” standard for UI in the Android platform sooner than later, and even if lots of apps will still use the View system, new screens will be coded using Compose instead, so it will become an unavoidable thing to learn. My strong suggestion is to dedicate some time to learn about its internals in-depth, since that will yield powerful skills to write modern and efficient Android apps. In the other hand, if you are interested in other use cases of Jetpack Compose rather than Android, you’ll likely be very happy to know that this book has got you covered also. Jetpack Compose internals is very focused on the compiler and runtime details, making the overall experience very agnostic of the target platform. Having an Android background should not be a requirement for reading the book. The book also provides a chapter dedicated to diverse use cases for Jetpack Compose, which exposes a few really interesting examples over code. What this book is not about This book does not try to replicate the Jetpack Compose official documentation, which is quite good already and the source of truth for any newcomers to the library. For that reason you will not find listings or catalogues with all the existing components or apis that the library provides in the book. If learning Compose is what you are looking for, I’d recommend you to go ahead and subscribe to the “Practical Jetpack Compose” by Joe Birch¹. Joe’s book is full of interesting examples and detailed explanations about all the relevant use cases of Jetpack Compose. That is a highly valuable reference book to have on your desk if you are an Android developer. The book is still under work, but will be released later this year. What this book is about This books heavily focuses on the internals of Jetpack Compose. As an Android developer and over the years, I have grown a feeling of how astoundingly important can become to learn about internals of the platform you work with every day. That helps me a lot to understand what code I want to write. Having that type of knowledge allows me to write performant code that complies to the platform expectations instead of going against them, and allows me to understand why things work the way ¹https://compose.academy/practicaljetpackcompose
Page
9
Prelude 2 they do. To me, this is probably one of the biggest differences between non very experienced and experienced Android developers. For many years we have all been diving into lower aspects of the platform like layout and draw passes, drawing efficiency, internals of the View system, styles and themes, lifecycles, and much more. This book is an opportunity to do the same but for Jetpack Compose, and open your mind in terms of how to think about it, given how important it is going to become for the years to come. My personal goal as the author of this book is to give you all the tools to achieve a big leap on this field. Keep the sources close If you ask me, I’d say that reading sources is one of the greatest skills we can acquire as software developers, let those be written by us, by our teammates or be part of any external libraries or languages. I strongly recommend anyone reading this book to keep the sources as close as possible while reading it, and explore even further. You can find everything in cs.android.com². Sources are also indexed in any Android Studio versions supporting Compose, so you should be able to navigate those. Having a playground project with Compose around is also desirable. Code snippets and examples One of the things we learn in this book is that Jetpack Compose can be used not only to represent UI trees but any large call graphs with generic node types. Still some of the code snippets and examples you’ll find in the book will be UI oriented for easier mental mapping, since that is what most developers are used to at this point. That said, this book includes a lesson that goes deep into how to use Jetpack Compose for diverse use cases, so that chapter contains snippets and examples that are not necessarily related to Android UI. Welcome to Jetpack Compose Internals. Grab a coffee, and enjoy your read. Jorge. ²https://cs.android.com/
Page
10
1. Composable functions The nature of Composable functions Probably the most indicate way to start a book about Jetpack Compose internals would be learning about Composable functions, given those are the atomic building blocks of Jetpack Compose, and the construct we will use to write our composable trees. I intentionally say “trees” here, since composable functions can be understood as nodes in a larger tree that the Compose runtime will be able to represent in memory. We will get to this in detail when the time comes, but it is good to start growing the correct mindset from the very beginning. If we focus on plain syntax, any standard Kotlin function can become a Composable function just by annotating it as @Composable: NamePlate.kt 1 @Composable 2 fun NamePlate(name: String) { 3 // Our composable code 4 } By doing this we are essentially telling the compiler that the function intents to convert data into a node to register in the composable tree. That is, if we read a Composable function as @Composable (Input) -> Unit, the Input would be the data, and the output (Unit) would not be a value returned from the function as most people would think, but an action registered to add the element to the in-memory representation of the composable tree. Note how returning Unit from a function that takes an input means we are likely consuming that input somehow within the body of the function. The described action is called “emitting” in the Compose jargon. It represents a scheduled change to the node tree. Composable functions emit when executed, and that happens during the Composition. We will get to that when we learn about the Jetpack Compose runtime in the following chapters.
Page
11
1. Composable functions 4 Composable function emits image But not all Composable functions return Unit though. Some of them return a value, and that changes their meaning. We could say that those are not “consuming” input, but “providing” a value something based on their input. One example of this can be remember. Remember.kt 1 Composable 2 fun NamePlate() { 3 val name = remember { generateName() } 4 Text(name) 5 } The remember Composable function allows to memoize the result of an operation and also return it. Yet, whenever this function executes, the in-memory representation of the tree will be updated with the relevant information from the call. That will include the call itself and its result. The ultimate takeout of this is to keep the in-memory representation of the tree always up to date with the structure and relevant information of our call graph. There are other relevant implications of annotating a function as Composable. The @Composable annotation effectively changes the type of the function or expression that it is applied to, and imposes some constraints or properties over it. These properties are very relevant to Jetpack Compose since they will unlock the library capabilities. The Compose runtime expects composable functions to comply to the mentioned properties, so it can assume certain behaviors and therefore exploit different runtime optimizations like parallel composition, arbitrary order of composition based on priorities, smart recomposition, or positional memoization among others. But please, don’t feel overwhelmed about all these new concepts yet, we will dive into every single one in depth at the right time. Generically speaking, runtime optimizations are only possible when a runtime can have some certainties about the code it needs to run, so it can assume specific conditions and behaviors from it. This unlocks the chance to execute, or in other words “consume” this code following different execution strategies or evaluation techniques that take advantage of the mentioned certainties. An example of these certainties could be the relation between the different elements in code. Are they dependant on each other or not? Can we run them in parallel or different order without affecting
Page
12
1. Composable functions 5 the program result? Can we interpret each atomic piece of logic as a completely isolated unit? Composable function properties Let’s learn about the properties of Composable functions. Calling context Any function that is annotated as @Composable gets translated by the Jetpack Compose compiler to a function that implicitly gets passed an instance of a Composer context as a parameter, and that also forwards that instance to its Composable children. We could think of it as an injected implicit parameter that the runtime and the developer can remain agnostic of. It looks something like this. Let’s say we have the following composable to represent a nameplate: NamePlate.kt 1 @Composable 2 fun NamePlate(name: String, lastname: String) { 3 Column(modifier = Modifier.padding(16.dp)) { 4 Text(text = name) 5 Text(text = lastname, style = MaterialTheme.typography.subtitle1) 6 } 7 } The compiler will add an implicit Composable parameter to each Composable call on the tree, plus some markers to the start and end of each composable. Note that the following code is simplified, but the result will be something like this: NamePlate after compiler processing. 1 fun NamePlate(name: String, lastname: String, $composer: Composer<*>) { 2 $composer.start(123) 3 Column(modifier = Modifier.padding(16.dp), $composer) { 4 Text( 5 text = name, 6 $composer 7 ) 8 Text( 9 text = lastname,
Page
13
1. Composable functions 6 10 style = MaterialTheme.typography.subtitle1, 11 $composer 12 ) 13 } 14 $composer.end() 15 } This gets the Composer context forwarded down the tree, so it will always be available at any level given the tree is conformed of Composable functions only. The compiler will make sure of this by imposing a very strict rule: Composable functions can only be called from other Composable functions. In other words, that will be the required calling context. By imposing this requirement, Jetpack Compose can ensure that the required information for the runtime is always accessible by any subtree. In the previous section we learned how Composable functions emit changes to the tree instead of yielding actual UI. The Composable will use the injected Composer instance to emit those changes during composition, and further recompositions will depend on the changes emitted by previous executions of the function. This is the connection between the production code we write as developers and the Compose runtime. This connection gives us the ability to inform the runtime about the shape of the tree so it can build its in memory representation of it and perform its magic. Don’t worry much if you don’t get this completey yet. This is something that will gradually become more clear in the lessons to come. For now, we can keep an idea of a declarative DSL (e.g: Compose UI) that we can use to emit actions to add, remove, or replace nodes to/from an in-memory representation of the composable tree. That representation can be used as a reference to materialize complete UIs later on. Idempotent Another property that Composable functions have is that they are expected to be idempotent regarding the program state. This means that we could call a Composable function a thousand times providing the same input data to it and it would always yield (emit) the same program state. The Jetpack Compose runtime relies on this assumption for things like recomposition. This book has a chapter dedicated to that, but we can have a sneak peek to showcase the point here. In Jetpack Compose, recomposition is the action of calling Composable functions again when the data they depend on varies, so they can emit updated elements. If we translate that to UI, the function
Page
14
1. Composable functions 7 could emit an updated or new node to the UI tree. Recomposition can happen for different reasons in Compose, and a function might get recomposed multiple times. That is why it is so important that Composable functions are idempotent. Otherwise we would be altering program state as a side effect of every recomposition. Recomposition is a vertical task that traverses down the tree checking which nodes need to be recomposed. When we refer to “smart recomposition”, what we mean is that the parts of the Composable tree that do not depend on the varied data can remain unchanged, so they are not recomposed. This is a big leap in terms of efficiency, since it means that those Composable lambdas are not called again. If we think twice about this we will notice that this is only possible because the state yielded by those functions is already stored and available in memory, and can be reused as is when their input has not varied. This is precisely a direct consequence of being idempotent. If our Composable functions would not yield the same program state for the same inputs every time, the runtime could never make that assumption and take any shortcuts in that matter. And this leads us directly to the next property. Free of side effects You might have heard the terms “pure functions” or “purity” before. This is very related to that. Pure functions are functions that don’t contain side effects. Let’s understand “side effect” as any action that escapes the scope of the function to do something unexpected on the side. In the context of Jetpack Compose, a side effect could be a change to the state of the app that happens outside of the scope of a Composable function. In broader terms, things like setting a global variable, updating a memory cache, or making a network query could also be considered side effects. What would happen if the network query fails? Or if the external cache gets updated between different executions of the function? The behavior of our function depends on those things. A side effect is indeed a source of ambiguity and makes the function non-deterministic. Side effects often lead to race conditions in programs. As you probably imagined already, the fact that a Composable function is idempotent also implies that the function must not run any uncontrolled side effects. Otherwise it could yield a different program state on every execution (i.e: composition), making it not idempotent. This effectively makes both properties really tied together. Allowing side effects would also imply that a Composable function might become dependant on the result of the execution of a previous Composable function. That must be avoided at all cost, since the runtime expects Composable functions to be able to execute in any order and even in parallel.
Page
15
1. Composable functions 8 That allows offloading recomposition to different threads and take advantage of multiple cores, for example. MainScreen.kt 1 @Composable 2 fun MainScreen() { 3 Header() 4 ProfileDetail() 5 EventList() 6 } The Header, ProfileDetail and EventList Composables might be executed in any order, so we can’t assume they’ll resolve sequentially. In this sense, we shouldn’t ever try to take advantage of an assumed order of composition to create logics based on that. One example of this could be to update a state from ProfileDetail with the intention to trigger a new effect on EventList in response. What would happen if EventList runs before or at the same time than ProfileDetail?. Another example could be setting a global variable from Header and reading it from EventList. What should the program do if we swap the order? Any relation we can establish between Composables via side effects of the composition is likely wrong and should be avoided. If we need to write business logic, that is not the responsibility of one or multiple Composable functions, and should probably be delegated on a different architecture layer. Compose has the ability to reorder composition based on priorities. One example is assigning lower priority to composables that are not on screen, in case we are using Jetpack Compose to represent UI. Another consequence of running side effects directly from a Composable function is that those could get called multiple times as a side effect of recomposition. That is too risky since it potentially compromises the integrity of our code and our application state, and creates race conditions. Imagine a Composable function that needs to fill up its state with data loaded from network: EventsFeed.kt 1 @Composable 2 fun EventsFeed(networkService: EventsNetworkService) { 3 val events = networkService.loadAllEvents() 4 5 LazyColumn { 6 items(events) { event -> 7 Text(text = event.name) 8 }
Page
16
1. Composable functions 9 9 } 10 } The effect here will get fired again for every recomposition, and we might end up with lots of effects taking place at the same time without any sort of control or coordination between them, only because the runtime might require to recompose this Composable many times in a very short period of time. But given side effects are required to write stateful programs, Jetpack Compose offers mechanisms to safely call effects from Composable functions by making them aware of the Composable lifecycle, so one can span a job across recompositions for example. Those are called effect handlers, and we will cover them in further chapters. For now, we can understand them as a safe environment to run our side effects so we avoid calling them directly in the Composable’s body. Another example of an issue that occurs when we run side effects in a Composable without any sort of control could be a composable updating some external variable holding a state. Since the function can potentially run from different threads, accessing that variable automatically becomes not thread-safe. Here is an example: BuggyEventFeed.kt 1 @Composable 2 fun BuggyEventFeed(events: List<Event>) { 3 var totalEvents = 0 4 5 Column { 6 Text(if (totalEvents == 0) "No events." else "Total events $totalEvents") 7 8 events.forEach { event -> 9 Text("Item: ${event.name}") 10 totalEvents++ // 11 } 12 } 13 } Here, totalEvents gets modified every time the Column composable gets recomposed. That means the total count will likely not match and it’s open to race conditions. Something interesting to realize as a final thought is how “free of side effects” is a requirement focused on the code the user writes. But truth is that building or updating Composition is actually a side effect of executing the Composable call graph. But this one is accepted (and required) structurally, since it unlocks the capabilities of the library, and it will not imply inconsistencies on our program behavior in any way.
Page
17
1. Composable functions 10 Restartable Composable functions are also expected to be restartable, which is something you have probably heard of or read somewhere else already. To clarify this property, this is just the same thing we have described in the previous sections. Composable functions can recompose, and therefore they are not like standard functions in a function chain, in the sense that they will not be called only once. Recomposing a Composable function means executing it again. Compose gives Composable functions the ability to recompose at any point in time, and only when required, so it can be selective about which nodes of the call graph to recompose, or in other words, restart. This is essentially a reactive approach where our functions can be re-executed based on changes in the state they are observing. All composable functions are restartable by default, since that is what the Compose runtime expects. Still, the runtime offers an annotation called NonRestartableComposable that can be used to remove this ability from a Composable function, so the compiler does not generate the required boilerplate needed to allow the function to recompose or be skipped. Please keep in mind that this has to be used very sparingly, since it might only make sense for very small functions that are likely getting recomposed (restarted) by another Composable function that calls them, since they might contain very little logic so it doesn’t make much sense for them to self invalidate. Their invalidation / recomposition will be essentially driven by their parent/enclosing Composable. We will cover this in detail in chapter 2. Fast execution Composable functions are expected to be fast, since they can be called multiple times. That is why they are designed to emit node changes to the Composable tree, instead of materializing actual UI right away. A Composable function could be called for every frame of an animation, for example. Any cost heavy computations should be offloaded to coroutines and always be wrapped into one of the lifecycle aware effect handlers that we will learn about ahead in this book. Making side effects lifecycle aware and suspended is the way Jetpack Compose compiler can ensure we use them correctly, and in coordination with what the Compose runtime expects. We can think of Composable functions and the Composable function tree as a fast, declarative, and lightweight approach to write a description of a program that will be retained in memory and interpreted / materialized later on.
Page
18
1. Composable functions 11 Positional memoization To understand this property we probably need to learn about “function memoization” first. Function memoization is the ability of a function to cache its result based on its inputs, so it does not need to be computed again every time the function is called for the same inputs. As we explained above, that is only possible for pure (deterministic) functions, since we have the certainty that they will always return the same result for the same inputs, hence we can save it and reuse it. Function memoization is a technique widely known in the world of Functional Programming, where programs are defined as a composition of pure functions and therefore memoizing the result of those functions can imply a big leap in performance. Positional memoization is based on this idea but with a key difference. Composable functions have constant knowledge about their location on the Composable tree. The runtime will differentiate calls to the same Composable function by providing them an identity that is unique within the parent. This identity is generated based on the position of the Composable function call, among other things. That way the runtime can differenciate the three calls to the Text() Composable function here: MyComposable.kt 1 @Composable 2 fun MyComposable() { 3 Text("Hello!") 4 Text("Hello!") 5 Text("Hello!") 6 } Those three are calls to the same Text composable, and they have the same inputs (none, in this case). But they are done from different places within the parent, hence the Composition will get three different instances of it, each one with a different identity. This identity is preserved over recompositions, so the runtime can appeal to the Composition to check whether a Composable was called previously or not, or whether it has changed. Sometimes generating that identity can be hard for the runtime, since it relies on the call position in the sources. There are cases where that position will be the same for multiple calls to the same Composable, and still represent different nodes. One example is lists of Composables generated from a loop:
Page
19
1. Composable functions 12 TalksScreen.kt 1 @Composable 2 fun TalksScreen(talks: List<Talk>) { 3 Column { 4 for (talk in talks) { 5 Talk(talk) 6 } 7 } 8 } Here, Talk(talk) is called from the same position every time, but each talk is expected to be different. In cases like this, the Compose runtime relies on the order of calls to generate the unique id and still be able to differentiate them. This works nicely when adding a new element to the end of the list, since the rest of calls stay in the same position they where before. But what if we add elements to the top, or the middle? The runtime will recompose all the Talks below that point since they changed their position, even if their inputs have not changed. This is inefficient and could lead to unexpected issues. In these cases, the runtime provides the key Composable so we can assign an explicit key to the call manually. TalksScreenWithKeys.kt 1 @Composable 2 fun TalksScreen(talks: List<Talk>) { 3 Column { 4 for (talk in talks) { 5 key(talk.id) { // Unique key 6 Talk(talk) 7 } 8 } 9 } 10 } This way we can associate each call to Talk() with a talk id, which will likely be unique, and this will allow the Composition to preserve identity of all the items on the list regardless of them changing positions. When we say that a Composable function “emits a node to the Composition”, all the information regarding the Composable call is stored, including their parameters, their inner Composable calls, the result of their remember calls, and any other relevant information. This is done via the injected Composer instance. Given Composable functions know about their location, any value cached by those will be cached only in the context delimited by that location. Here is an example for more clarity:
Page
20
1. Composable functions 13 FilteredImage.kt 1 @Composable 2 fun FilteredImage(path: String) { 3 val filters = remember { computeFilters(path) } 4 ImageWithFiltersApplied(filters) 5 } 6 7 @Composable 8 fun ImageWithFiltersApplied(filters: List<Filter>) { 9 TODO() 10 } Here we use remember to cache the result of a heavy operation to precompute the filters of an image given its path. Once we compute them, we can render the image with the already computed filters. Caching the result of the precomputation is desirable. The key for indexing the cached value will be based on the call position in the sources, and also the function input, which in this case is the file path. The remember function is a Composable function that knows how to read from the slot table in order to get its result. When the function is called it will look for the function call on the table, and return the cached result when available. Otherwise it will compute it and store the result before returning it, so it can be retrieved later. In Jetpack Compose, memoization is not the traditional “application-wide” memoization. Here, remember leverages positional memoization to get the cached value from the context delimited by the Composable calling it: FilteredImage. Meaning that it goes to the table and looks for the value in the range of slots where the information for this Composable is expected to be stored. This makes it more like a singleton within that scope, since it will compute the value only during the initial composition, but for each recomposition it will retrieve the cached value. But if the same Composable was used in a different composition, or the same function call was remembered from a different composable, the remembered value would be a different instance. Compose is built on top of the concept of positional memoization, since that is what smart recomposition is based on. The remember Composable function simply makes use of it explicitly for more granular control. This post by Leland Richardson from the Jetpack Compose team at Google explains positional
The above is a preview of the first 20 pages. Register to read the complete e-book.
AI Reading Assistant
Whole-book reading guide from stratified index samples; jump to passages in the text
AI guide
【One-Line Pitch】
A deep, source-guided tour of how Jetpack Compose actually works under the hood—from the compiler plugin that rewrites `@Composable` functions to the runtime that drives composition and recomposition—for Android developers who already write Compose and want to reason about it like a platform engineer.
【Book Arc】
- **Opening (~0%–10%)**: Sets expectations and scope—why internals matter, what the book is *not* (not a UI tutorial), and how to read alongside the Compose sources and a playground project. Establishes the mindset shift from View-system thinking to declarative composition.
- **Early (~10%–30%)**: Builds the foundation around Composable functions—their nature, the calling-context requirement, and the core properties (idempotent, side-effect-free, restartable, fast, positionally memoized). Draws the parallel with `suspend` functions and explains why Composables are "colored."
- **Middle (~30%–55%)**: Moves into the Compose compiler as a Kotlin compiler plugin—annotations, static checkers (call/type/declaration), diagnostics, and code generation via the Kotlin IR. Explains groups, `@ReadOnlyComposable`, and stability annotations that let the runtime skip work.
- **Late (~55%–85%)**: Turns to the runtime and Compose UI—how the in-memory composition tree is built and updated, recomposition, positional memoization, and how the runtime can be reused beyond Android UI (the excerpts gesture at a standalone composition in the browser).
- **Ending (~85%–100%)**: Closes with a conclusion and the broader lesson that Compose can represent any large call graph with generic node types, not just UI trees—encouraging readers to keep exploring the sources.
【Key Takeaways】
- **`@Composable` changes a function's type and imposes a contract** (Early): the annotation isn't cosmetic—it enforces a calling context and properties the runtime depends on for optimizations like parallel composition and smart recomposition.
- **Composables must be idempotent and side-effect-free** (Early): the runtime may call them in any order or repeatedly, so business logic and network calls belong in another layer, not inside composition.
- **Restartability is the reactive core** (Early): Composables re-execute when observed state changes, which is what makes selective recomposition possible.
- **Positional memoization ties cached values to call-site identity** (Early): `remember` caches only within the location delimited by that call, and `key` lets you preserve identity across list reordering—without it, moving items forces needless recomposition.
- **The compiler is a Kotlin compiler plugin doing frontend static analysis** (Middle): call/type/declaration checkers give fast, IDE-integrated feedback, while IR transformation rewrites Composables so the runtime can support them.
- **Groups are the runtime's bookkeeping unit** (Middle): the compiler emits groups (restartable, movable, etc.) with source-position keys, enabling cleanup, movement, and positional memoization; `@ReadOnlyComposable` lets it skip unnecessary codegen.
- **Stability annotations unlock skipping** (Middle): when parameters are stable/immutable or primitive, the runtime can compare values and skip recomposition—making stability a performance lever, not just a formality.
- **Compose generalizes beyond UI** (Late): the runtime can model any large call graph with generic node types, which is why writing your own runtime client is a legitimate advanced use case.
【Reading Tips】
- **Read with the sources open**: the author explicitly recommends keeping `cs.android.com` and a playground project nearby; navigate the real Compose code as concepts appear.
- **Deep-read the Early and Middle sections**: Composable properties and the compiler chapter are the conceptual spine—everything later depends on them.
- **Skim the compiler-plugin plumbing** (extension registration, IR lowering details) on a first pass unless you do metaprogramming; return to it when debugging stability or codegen surprises.
- **Treat stability and `key` as practical takeaways**: after each chapter, audit one screen in your own app for unstable parameters or unkeyed lists.
- **Don't expect a UI cookbook**: this is an internals book; pair it with hands-on Compose work to convert theory into intuition.
【Coverage Limits】
The excerpts are heavily front-loaded (table of contents plus the Composable-functions and compiler chapters), so later runtime, Compose UI, and browser-composition material is only gestured at; specific chapter titles and figures beyond those shown are not covered here.
Passage locations
Page 5
. . . . 10 Positional memoization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Similarities with suspend functions ....
View in text
Page 11
will dive into every single one in depth at the right time. Generically speaking, runtime optimizations are only possible when a runtime can have some certai...
View in text
Excerpt 3
irement of a calling context for Composable functions might have raised some eyebrows, since it might remind to another language primitive available in Kotli...
View in text
Excerpt 4
lightweight compiler plugins” that any libraries can start relying on for metaprogramming. Make sure to give a read to the “Why KSP” section in the KSP repos...
View in text
Recommended for You
{{#thumbnailUrl}}
{{/thumbnailUrl}}
{{^thumbnailUrl}}
{{/thumbnailUrl}}
Loading recommended books...
Failed to load, please try again later
Tip the Site
Scan the WeChat Pay or Alipay code to tip. No login required.
WeChat Pay
Alipay