Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Visualizing Compose IR Transformations

Avatar for kitakkun kitakkun
November 29, 2025

Visualizing Compose IR Transformations

「Visualizing Compose IR Transformations プラグイン実装から解き明かすComposable関数の正体」

DroidKaigi.collect { #28@Fukuoka } の発表資料です。

Avatar for kitakkun

kitakkun

November 29, 2025
Tweet

More Decks by kitakkun

Other Decks in Programming

Transcript

  1. @Composable function → UI Component 3 @Composable fun A() {

    Column { Text("Hello") Text("World!") } }
  2. @Composable function → UI Component 4 @Composable fun A() {

    var count by remember { mutableIntStateOf(0) } Button( onClick = { count++ } ) { Text("$count") } }
  3. @Composable fun A( x: Int, $composer: Composer?, $changed: Int, )

    Adding extra value parameters by ComposerParamTransformer 9
  4. @Composable fun A( x: Int = 10, $composer: Composer?, $changed:

    Int, ) Adding extra value parameters by ComposerParamTransformer 10
  5. Adding extra value parameters by ComposerParamTransformer 11 @Composable fun A(

    x: Int, $composer: Composer?, $changed: Int, $default: Int, )
  6. Adding extra value parameters by ComposerParamTransformer 12 @Composable fun A(

    x: Int, $composer: Composer?, $changed: Int, $default: Int, )
  7. Bitmask allocation for $default 15 @Composable fun A( x: Int

    = 10, ) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 high l ow x 1 :de f a ul t va lu e p r esent 0 :no de f a ul t va lu e @Composable fun A( x: Int, $default: Int, )
  8. Bitmask allocation for $default 16 0 0 0 0 0

    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 high l ow x 1 :de f a ul t va lu e p r esent 0 :no de f a ul t va lu e y @Composable fun A( x: Int = 10, y: Int, ) @Composable fun A( x: Int, y: Int, $default: Int, )
  9. Bitmask allocation for $default 17 0 0 0 0 0

    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 high l ow x 1 :de f a ul t va lu e p r esent 0 :no de f a ul t va lu e y z @Composable fun A( x: Int = 10, y: Int, z: Int = 20, ) @Composable fun A( x: Int, y: Int, z: Int, $default: Int, )
  10. Bitmask allocation for $changed 19 @Composable fun A( x: Int,

    $changed: Int, ) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 x Rese r ved bit (1: f o r ce r ecomposition) high l ow
  11. Bitmask allocation for $changed 20 @Composable fun A( x: Int,

    y: Int, $changed: Int, ) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 y x high l ow Rese r ved bit (1: f o r ce r ecomposition)
  12. Bitmask allocation for $changed 21 @Composable fun B.A( x: Int,

    y: Int, $changed: Int, ) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 y x high l ow <this> ※B Rese r ved bit (1: f o r ce r ecomposition)
  13. Bitmask allocation for $changed 22 context(z: Z) @Composable fun B.A(

    x: Int, y: Int, $changed: Int, ) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 y x high l ow <this> ※B z Rese r ved bit (1: f o r ce r ecomposition)
  14. Bitmask allocation for $changed 23 class C { context(z: Z)

    @Composable fun B.A( x: Int, y: Int, $changed: Int, ) } high l ow <this> ※C Rese r ved bit (1: f o r ce r ecomposition) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 y x <this> ※B z
  15. Parameter-state bits for $changed ParamState in ComposableFunctionBobyTransformer.kt 24 0 0

    0 Unce r tain 0 0 1 Same 0 1 0 Di ff e r ent 0 1 1 Static 1 0 0 Un k nown 1 1 1 Mas k
  16. $dirty bitmask calculation 27 @Composable fun A( x: Int, //

    default: 10 $composer: Composer?, $changed: Int, $default: Int, ) { f(x) // Composable }
  17. $dirty bitmask calculation 29 @Composable fun A(x: Int, $composer: Composer?,

    $changed: Int, $default: Int) { var $dirty = $changed // f(x) }
  18. $dirty bitmask calculation 30 @Composable fun A(x: Int, $composer: Composer?,

    $changed: Int, $default: Int) { var $dirty = $changed if ($default and 0b0001 != 0) { // default value presents } // f(x) }
  19. $dirty bitmask calculation 31 @Composable fun A(x: Int, $composer: Composer?,

    $changed: Int, $default: Int) { var $dirty = $changed if ($default and 0b0001 != 0) { // default value presents $dirty = $dirty and 0b0110 // Static } // f(x) } 0 0 0 Unce r tain 0 1 0 Di ff e r ent 0 0 1 Same 0 1 1 Static 1 0 0 Un k nown
  20. $dirty bitmask calculation 32 @Composable fun A(x: Int, $composer: Composer?,

    $changed: Int, $default: Int) { var $dirty = $changed if ($default and 0b0001 != 0) { // default value presents $dirty = $dirty and 0b0110 // Static } else if ($changed and 0b0110 == 0) { // Uncertain or Unknown } // f(x) } 0 0 0 Unce r tain 0 1 0 Di ff e r ent 0 0 1 Same 0 1 1 Static 1 0 0 Un k nown
  21. $dirty bitmask calculation 33 @Composable fun A(x: Int, $composer: Composer?,

    $changed: Int, $default: Int) { var $dirty = $changed if ($default and 0b0001 != 0) { // default value presents $dirty = $dirty and 0b0110 // Static } else if ($changed and 0b0110 == 0) { // Uncertain or Unknown $dirty = $dirty or if ($composer.changed(x)) 0b0100 // Different else 0b0010 // Same } // f(x) } 0 0 0 Unce r tain 0 1 0 Di ff e r ent 0 0 1 Same 0 1 1 Static 1 0 0 Un k nown
  22. @Composable fun A(x: Int, $composer: Composer?, $changed: Int, $default: Int)

    { var $dirty = } var $dirty = … // calculate $dirty bitmask… if ( $composer.shouldExecute( $dirty and 0b0011 != 0b0010, // state of x was changed $dirty and 0b0001 // force recomposition ) ) { } } Recompose with $dirty 34 0 0 0 Unce r tain 0 1 0 Di ff e r ent 0 0 1 Same 0 1 1 Static 1 0 0 Un k nown
  23. @Composable fun A(x: Int, $composer: Composer?, $changed: Int, $default: Int)

    { var $dirty = … // calculate $dirty bitmask… if ( $composer.shouldExecute( $dirty and 0b0011 != 0b0010, // state of x was changed $dirty and 0b0001 // force recomposition ) ) { } } Recompose with $dirty 35 0 0 0 Unce r tain 0 1 0 Di ff e r ent 0 0 1 Same 0 1 1 Static 1 0 0 Un k nown @Composable fun A(x: Int, $composer: Composer?, $changed: Int, $default: Int) { var $dirty = … // calculate $dirty bitmask… if ( $composer.shouldExecute( $dirty and 0b0011 != 0b0010, // state of x was changed $dirty and 0b0001 // force recomposition ) ) { if ($default and 0b0001 != 0) x = 10 f(x, $composer, 0b0110) // x is Static from here } }
  24. What is Group? Replace Group 38 @Composable fun A(flag: Boolean)

    { if (flag) { Text("true") } else { Text("false") } } ReplaceGroup Text("true") ReplaceGroup Text("false") RestartGroup A
  25. What is Group? Movable Group 39 @Composable fun A(items: List<Int>)

    { Column { for (x in items) { key(x) { Text("$x") } } } } MovableGroup key(x) ReplaceGroup Column(lamba) RestartGroup A
  26. Types of Group • Resta r tab l e G

    r o u p • ؔ਺ͷ࠶࣮ߦڥքɺεΩοϓՄೳͳάϧʔϓ • Rep l aceab l e G r o u p • ஔ׵Մೳͳ୯Ґʢi f -e l se, whenͳͲ੍ޚϑϩʔʹඥͮ͘άϧʔϓʣ • Movab l e G r o u pʢ k e y ͳͲͰੜ੒ʣ • ݺͼग़͠ॱʹґଘͤͣɺS l otͷҐஔΛॊೈʹҠಈͰ͖Δάϧʔϓ 40
  27. 43 @Composable fun A( x: Int, // default: 10 $composer:

    Composer?, $changed: Int, $default: Int, ) { f(x) // Composable }
  28. 44 @Composable fun A(x: Int, $composer: Composer?, $changed: Int, $default:

    Int) { var $dirty = … // calculate $dirty bitmask… }
  29. @Composable fun A(x: Int, $composer: Composer?, $changed: Int, $default: Int)

    { var $dirty = … // calculate $dirty bitmask… if ( $composer.shouldExecute( $dirty and 0b0011 != 0b0010, // state of x was changed $dirty and 0b0001 // force recomposition ) ) { if ($default and 0b0001 != 0) x = 10 f(x, $composer, 0b0110) // x is Static from here } } 45
  30. @Composable fun A(x: Int, $composer: Composer?, $changed: Int, …) {

    var $dirty = … if ($composer.shouldExecute(…)) { … } } 46
  31. @Composable fun A(x: Int, $composer: Composer?, $changed: Int, …) {

    $composer.startRestartGroup(-1635445003) var $dirty = … if ($composer.shouldExecute(…)) { … } } 47 DurableFunctionKey Transformer Gene r ates k e y
  32. @Composable fun A(x: Int, $composer: Composer?, $changed: Int, …) {

    $composer.startRestartGroup(-1635445003) var $dirty = … if ($composer.shouldExecute(…)) { … } else { $composer.skipToGroupEnd() } } 48
  33. @Composable fun A(x: Int, $composer: Composer?, $changed: Int, …) {

    $composer.startRestartGroup(-1635445003) var $dirty = … if ($composer.shouldExecute(…)) { … } else { $composer.skipToGroupEnd() } $composer.endRestartGroup()?.updateScope { $composer, $force -> A(x, $composer, $changed or 0b1) } } 49
  34. @Composable fun A(x: Int, $composer: Composer?, $changed: Int, …) {

    $composer.startRestartGroup(-1635445003) var $dirty = … if ($composer.shouldExecute(…)) { if ($default and 0b0001 != 0) x = 10 f(x, $composer, 0b0110) } else { $composer.skipToGroupEnd() } $composer.endRestartGroup()?.updateScope { $composer, $force -> A(x, $composer, $changed or 0b1) } } 51 Stable
  35. StrongSkipping disabled @Composable fun A(x: UnstableType, $composer: Composer?, $changed: Int,

    …) { $composer.startRestartGroup(1096661310) f(x, $composer, 0) $composer.endRestartGroup()?.updateScope { $composer, $force -> A(x, $composer, $changed or 0b1) } } 52 Unstable
  36. What Stability means? Determines if Composable function is Skippable or

    not • ҆ఆʢStab l eʣ • $changed Λ࢖ͬͨߴ଎ͳ౳Ձ൑ఆ͕Մೳ • ঢ়ଶมԽΛ҆શʹ௥੻Ͱ͖Δ → εΩοϓՄೳ • ෆ҆ఆʢUnstab l eʣ • ࣮͔֬ͭߴ଎ͳൺֱखஈ͕ͳ͘ݪଇεΩοϓෆՄೳ • ྫ֎ɿSt r ong S k ipping Mode༗ޮ࣌͸ࢀরൺֱʹΑΓεΩοϓ͞ΕΔ 53
  37. @Composable fun A(x: UnstableType, $composer: Composer?, $changed: Int, …) {

    $composer.startRestartGroup(1096661310) f(x, $composer, 0) $composer.endRestartGroup()?.updateScope { $composer, $force -> A(x, $composer, $changed or 0b1) } } 54 StrongSkipping disabled
  38. @Composable fun A(x: UnstableType, $composer: Composer?, $changed: Int, …) {

    $composer.startRestartGroup(1096661310) var $dirty = $changed if ($dirty and 0b0110 == 0) { $dirty = dirty or if ($composer.changedInstance(x)) 0b0100 else 0b0010 } if ($composer.shouldExecute(…)) { f(x, $composer, 0) } else { $composer.skipToGroupEnd() } $composer.endRestartGroup()?.updateScope { $composer, $force -> A(x, $composer, $changed or 0b1) } } 55 StrongSkipping enabled
  39. Stability Inference StabilityInferencer in Stability.kt • Composab l eؔ਺ͷ֤ύϥϝʔλͷܕͷ҆ఆੑΛਪ࿦͢Δ •

    Ce r tain →ɹίϯύΠϧλΠϜͰ֬ఆ • R u ntime →ɹ࣮ߦ࣌ʹܾఆ • Un k nown →ɹෆ໌ • Pa r amete r →ɹܕҾ਺ʹґଘ • Combined → ෳ਺ͷStabi l it y ͕ෳ߹ͨ͠΋ͷ 57
  40. / / va l f : () -> Unit va

    l fu nction: F u nction <* > / / va l k f : (St r ing) -> Boo l ean = St r ing :: isEmpt y va l k F u nction: K F u nction <*> All Stable Function type? … ❌No ✅Yes Stable …
  41. va l st r ing: St r ing Stable String

    type? … ❌No ✅Yes Stable …
  42. Is primitive type? … ❌No ✅Yes Stable va l boo

    l ean: Boo l ean va l cha r : Cha r va l b y te: B y te va l sho r t: Sho r t va l int: Int va l fl oat: F l oat va l l ong: Long va l do u b l e: Do u b l e … All Stable
  43. @Stab l e c l ass A { … }

    @Imm u tab l e c l ass B { … } @Stab l e open c l ass C c l ass D : C() All Stable has StableMarker on itself or super type? … ❌No ✅Yes Stable …
  44. en u m A { B, C, D, } Is

    Enum or EnumEntry? … ❌No ✅Yes … All Stable Stable
  45. s y ntax = "p r oto3"; message Samp l

    e { boo l is_enab l ed = 1; } Is Protobuf type? … ❌No ✅Yes … Stable Stable p u b l ic f ina l c l ass Samp l e p r ivate const ru cto r ( va l name: St r ing = "", va l id: Long = 0L, ): com.goog l e.p r otob uf .Gene r ateMessageLite <Samp l e,Samp l e.B u i l de r > (DEFAULT_INSTANCE),
  46. // Stabi l it y In f e r ence

    r .stabi l it y O f (I r C l ass, …) va l dec l a r ation: I r C l ass i f ( canIn f e r Stabi l it y (dec l a r ation) | | dec l a r ation.isExte r na l Stab l eType() ) { … } Can infer stability or external stable type? … ❌No ✅Yes … Extra Logic
  47. object K nownStab l eConst r u cts { va

    l stab l eT y pes = mapO f ( Pai r :: c l ass.q u a l i f iedName !! to 0b11, … // G u ava "com.goog l e.common.co ll ect.Imm u tab l eList" to 0b1, … // K ot l inx imm u tab l e " k ot l inx.co ll ections.imm u tab l e.Imm u tab l eCo ll ection" to 0b1, " k ot l inx.co ll ections.imm u tab l e.Imm u tab l eList" to 0b1, … // Dagge r "dagge r .Laz y " to 0b1, // Co r o u tines Empt y Co r o u tineContext :: c l ass.q u a l i f iedName !! to 0, // Java t y pes BigIntege r:: c l ass.q u a l i f iedName !! to 0, BigDecima l:: c l ass.q u a l i f iedName !! to 0, ) Can infer stability or external stable type? ✅Yes Is known stable type? ✅Yes (maybe)Stable … ❌No
  48. ❌No // Sing l e c l ass com.examp l

    e.Exte r na l T y pe // Pac k age wi l dca r d com.examp l e.mode l s.** // Sing l e-segment wi l dca r d com.examp l e.*.data // Gene r ic pa r amete r inc lu sion com.examp l e.Containe r<*> // Gene r ic pa r amete r exc lu sion com.examp l e.W r appe r<* ,_> // Mixed gene r ic pa r amete r s com.examp l e.Comp l ex <* ,_, *> Is known stable type? Is external stable type? ✅Yes (maybe)Stable … ❌No https: // gith u b.com/s ky doves/compose-stabi l it y -in f e r ence? tab= r eadme-ov- f i l e#62-con f ig ur ation- f i l es stabi l it y _con f ig.con f
  49. For more information • compose-stabi l it y -in f

    e r ence b y s ky doves͞Μ • https: // gith u b.com/s ky doves/compose-stabi l it y -in f e r ence?tab= r eadme- ov- f i l e#27-stabi l it y -decision-t r ee • ίϯύΠϥϓϥάΠϯͷ࣮૷ʹج͍ͮͯΘ͔Γ΍͘͢੔ཧͨ͠ ϑϩʔνϟʔτ͕࡞ΒΕ͍ͯ·͢ 69
  50. Summary • Va lu e pa r amete r t

    r ans f o r mation • $compose r , $changed, $de f a ul t • $changed, $de f a ul t, $compose r .changed o r changedInstance → $di r t y → $compose r .sho ul dExec u te • Resta r t/Rep l ace/Movaba l e G r o u ps f o r node management • IR di ff e r ences when pa r amete r s a r e stab l e o r u nstab l e • Stabi l it y In f e r ence a l go r ithm 70