Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Branching out to Jetpack Compose

Branching out to Jetpack Compose

Talk with Nacho López: https://twitter.com/mrmans0n

As one of the most widely used social media platforms, Twitter is always hunting for ways to better connect its users. In early 2021 the Client UI team at Twitter began the task of integrating Jetpack Compose into the Twitter for Android app, with the goal of allowing developers to efficiently write new UI features.

In this talk, Chris & Nacho will outline the process which the team has undertaken to adopt Jetpack Compose sustainably. They will explore the static tooling which has been written to guide developers instantly, how our design system components have evolved in that time, the processes that have been established to support feature teams. We’ll also cover some of the mistakes that we’ve made along the way.

86f746f6c0c522ab2261bd55791d8a4a?s=128

Chris Banes

June 03, 2022
Tweet

More Decks by Chris Banes

Other Decks in Programming

Transcript

  1. Branching out to Jetpack Compose Chris Banes @chrisbanes Nacho López

    @mrmans0n !
  2. !

  3. Optional section title 3

  4. Compose is on your device

  5. ...but how?

  6. What we did was not perfect

  7. What we did was not perfect Your journey will be

    different
  8. 8 Our journey

  9. Twitter for Android

  10. 1000+ modules

  11. 300+ UI modules

  12. 30+ teams

  13. Late 2020: How do we make UI dev faster?

  14. How do we start using Compose? Late 2020:

  15. 16 We started with a Button Hi friends! Hi friends!

    Hi friends! Hi friends! Hi friends!
  16. 17 First, we had to sort out our theming ONE

    DOES NOT SIMPLY WRITE A COMPOSE COMPONENT
  17. 18 Design system Doesn't map 1:1 to Material

  18. How do we implement our design system in Compose?

  19. Lots of layers to build upon Compose is layered Runtime

    Fundamental parts of the Compose runtime UI Fundamental building blocks for the Compose UI Toolkit Foundation Design system agnostic components such as base layouts and animation Material Opinionated implementation of the Material Design system for Compose UI
  20. Lots of layers to build upon Compose is layered Runtime

    Fundamental parts of the Compose runtime UI Fundamental building blocks for the Compose UI Toolkit Foundation Design system agnostic components such as base layouts and animation Material Opinionated implementation of the Material Design system for Compose UI Opinionated implementation of the Material Design system for Compose UI Material Compose
  21. Runtime Fundamental parts of the Compose runtime UI Fundamental building

    blocks for the Compose UI Toolkit Foundation Design system agnostic components such as base layouts and animation Material Opinionated implementation of the Material Design system for Compose UI Compose Android framework Fundamental parts of the Compose runtime android.view.* Mostly fundamental UI layer. Contains some Material concepts and styling AppCompat + AndroidX Backports framework functionality Material Design Components Provides off-the-shelf components for Material Views
  22. Runtime Fundamental parts of the Compose runtime UI Fundamental building

    blocks for the Compose UI Toolkit Foundation Design system agnostic components such as base layouts and animation Material Opinionated implementation of the Material Design system for Compose UI Compose Android framework Fundamental parts of the Compose runtime android.view.* Mostly fundamental UI layer. Contains some Material concepts and styling AppCompat + AndroidX Backports framework functionality Material Design Components Provides off-the-shelf components for Material Views Layers which contain Material
  23. Most apps with their own design system will be somewhere

    in the middle Material 0
  24. To maintain platform consistency Touch highlights App chrome Elevation Material

    0
  25. Material 0 Easier in Compose Build on Foundation

  26. Material 0 Easier in Compose Build on Foundation Easier in

    Compose Everything is in one place and built as one
  27. Material 0 Easier in Compose Build on Foundation Easier in

    Compose Everything is in one place and built as one Easier in views More difficult to use Compose components without using Material theming / concepts
  28. You either: Big decision alert map your design system to

    Material or start afresh using Compose Foundation
  29. You either: Big decision alert map your design system to

    Material or start afresh using Compose Foundation We started out here
  30. You either: Big decision alert map your design system to

    Material or start afresh using Compose Foundation We're moving to here We started out here
  31. 2021 April May June July We had built out some

    of the core components and theming How do we start using Compose?
  32. 2021 April May June July We had built out some

    of the core components and theming How do we actually use Compose across the app?
  33. We found 4 teams who were eager to adopt Compose

    EAPs Started small to work with the teams as closely as possible Very informal Early access partners
  34. Each team was soon to begin writing new UI EAPs

    Different requirements: lists, paging, text input, navigation Acted as a way to crowdsource our priority list Early access partners
  35. We quickly found that supporting 4 teams concurrently was a

    lot of work EAPs We were providing in-depth feedback, and filling gaps Early access partners
  36. EAPs Early access partners Other non-EAP teams then started using

    Compose, adding to the workload Those teams weren't always ready to support Compose
  37. 2021 mber December How do we spend less time on

    support?
  38. List of modules which are 'allowed' to use Compose Allowlist

    Shepherds / / Teams could request to be added to the allowlist Enabled us to find out what support was required before they started ...also allowed us to set expectations on what support we could provide
  39. final def composeAllowedModules = project .fileContents("${project.rootDir}/path/to/allowlist.txt") .lines() final String modulePath

    = project.path final boolean moduleInComposeAllowlist = composeAllowedModules.anyMatch { it !" modulePath } if (!moduleInComposeAllowlist) { !# If the module is using Jetpack Compose but isn't in the allowlist!!$! tasks.withType(KotlinCompile).configureEach { task !% task.doFirst { !# We check to see if the Compose Compiler plugin has !# been added to the compiler args. final boolean usingComposeCompiler = (task as KotlinCompile) .kotlinOptions .freeCompilerArgs .any { it.contains('androidx.compose.compiler') } Allowlist Shepherds / / bit.ly/compose-allowlist
  40. .fileContents("${project.rootDir}/path/to/allowlist.txt") .lines() final String modulePath = project.path final boolean moduleInComposeAllowlist

    = composeAllowedModules.anyMatch { it !" modulePath } if (!moduleInComposeAllowlist) { !# If the module is using Jetpack Compose but isn't in the allowlist!!$! tasks.withType(KotlinCompile).configureEach { task !% task.doFirst { !# We check to see if the Compose Compiler plugin has !# been added to the compiler args. final boolean usingComposeCompiler = (task as KotlinCompile) .kotlinOptions .freeCompilerArgs .any { it.contains('androidx.compose.compiler') } if (usingComposeCompiler) { throw new GradleException(!!$) } } } } Allowlist Shepherds / / bit.ly/compose-allowlist
  41. Allowlist Shepherds / / Request form Are you available to

    spend the time learning Compose? Please state if you and your team will have enough time to onboard to Compose.
  42. Allowlist Shepherds / / Request form Are you available to

    spend the time learning Compose? Please state if you and your team will have enough time to onboard to Compose. Do you have the time to iterate on Client UI's feedback? Please let us know if you have urgent time concerns to ship your feature.
  43. Allowlist Shepherds / / Request form Are you available to

    spend the time learning Compose? Please state if you and your team will have enough time to onboard to Compose. Do you have the time to iterate on Client UI's feedback? Please let us know if you have urgent time concerns to ship your feature. What features from Compose do you need? Describe the features you anticipate you might need. If migrating from views, do you have metrics to make sure nothing regresses?
  44. Allowlist Shepherds / / Request form Are you available to

    spend the time learning Compose? Please state if you and your team will have enough time to onboard to Compose. Do you have the time to iterate on Client UI's feedback? Please let us know if you have urgent time concerns to ship your feature. What features from Compose do you need? Describe the features you anticipate you might need. If migrating from views, do you have metrics to make sure nothing regresses? Is your whole team on board? Let us know whether your team is onboard and willing to do code reviews for this feature.
  45. Allowlist Shepherds / / A group of engineers there to

    'shepherd' Compose adoption W ork-in-p rogress
  46. Allowlist Shepherds / / A group of engineers there to

    'shepherd' Compose adoption Automatically added to all Compose code reviews Keep an eye on Compose usage and trends W ork-in-p rogress
  47. Allowlist Shepherds / / A group of engineers there to

    'shepherd' Compose adoption Goal: Expanding the group beyond just the Client UI team Graduating engineers from EAP teams Automatically added to all Compose code reviews W ork-in-p rogress
  48. June 2022

  49. 60+ Compose modules

  50. 13 Teams using Compose

  51. Using Compose at "

  52. Twitter for Android First commit in the repo from 2010

  53. Twitter for Android First commit in the repo from 2010

    A lot of custom infra built in
  54. Twitter for Android First commit in the repo from 2010

    A lot of custom infra built in Infra usually created before OSS alternatives
  55. Examples of custom infra UI

  56. UI DI Examples of custom infra

  57. All our custom infra is properly val viewModel = weaverViewModel<MyViewModel>()

    val state by viewModel.watchAsState() supported
  58. val viewModel = weaverViewModel<MyViewModel>() val state by viewModel.watchAsState() val myObject

    = viewSubgraph<MySubgraph>().myObject All our custom infra is properly supported
  59. + ...and so on All our custom infra is properly

    documented
  60. 71 documented properly

  61. 72

  62. 72

  63. 72

  64. 73 Documentation for all levels

  65. 74 Codelabs are great starter docs

  66. 75 Codelabs

  67. None
  68. None
  69. Gotchas should be documented too

  70. Gotchas Using custom emoji is not possible yet Interop performance

    when in a RecyclerView Complex accessibility is limited And others... it's a live document!
  71. None
  72. Best practices

  73. Tools, not rules Best practices?

  74. Tools, not rules is involved Tooling We'll get there, but

  75. Tools Infra &

  76. Tools P re v ie w s Infra &

  77. Previews are pretty picky

  78. are pretty picky Previews

  79. 87

  80. 88

  81. 89 from Dolphin+ Previews and multiple th

  82. 90 Previews and multiple themes from Dolphin+

  83. 91 Standard Lights out Dim 3 themes need previews Previews

  84. 92 enum class ThemeVariant(private val themeSuffix: String) { } STANDARD(".Standard"),

    DIM(".Dim"), LIGHTS_OUT(".LightsOut"); !# !!$
  85. 93 class ThemeVariantPreviewProvider : PreviewParameterProvider<ThemeVariant> { override val values =

    ThemeVariant.values().asSequence() } enum class ThemeVariant(private val themeSuffix: String) { } !!$
  86. 94 @Preview @Composable fun MyPreview( ) { HorizonTheme(themeVariant = variant)

    { MyComposable() } } @PreviewParameter(ThemeVariantPreviewProvider!&class) variant: ThemeVariant class ThemeVariantPreviewProvider : PreviewParameterProvider<ThemeVariant> { override val values = ThemeVariant.values().asSequence() }
  87. 94 @Preview @Composable fun MyPreview( ) { HorizonTheme(themeVariant = variant)

    { MyComposable() } } @PreviewParameter(ThemeVariantPreviewProvider!&class) variant: ThemeVariant class ThemeVariantPreviewProvider : PreviewParameterProvider<ThemeVariant> { override val values = ThemeVariant.values().asSequence() }
  88. @Preview @Composable fun MyPreview( ) { HorizonTheme(themeVariant = variant) {

    MyComposable() } } @PreviewParameter(ThemeVariantPreviewProvider!&class) variant: ThemeVariant We use our own CompositePreviewProvider @PreviewParameter( ) @PreviewParameter( ) !&class https://bit.ly/composite-preview-provider
  89. 96 @Preview @Composable fun MyPreview( ) { val (variant, myData)

    = model HorizonTheme(themeVariant = variant) { MyComposable(myData) } } @PreviewParameter( ) !&class MyPreviewProvider model: Pair<ThemeVariant, MyData> val (variant, myData) = model themeVariant = variant myData https://bit.ly/composite-preview-provider
  90. class MyPreviewProvider : PreviewParameterProvider<Pair<ThemeVariant, MyData!' by compositeProvider( ThemeVariantPreviewProvider!&class, MyDataPreviewProvider!&class )

    https://bit.ly/composite-preview-provider
  91. 98 Previews and custom infra run in their own A

    that we don't c
  92. 99 Previews run in their own Activity that we don't

    control
  93. 100 val viewModel = weaverViewModel<MyViewModel>() val subgraph = viewSubgraph<MySubgraph>() val

    starter = rememberContentViewStarter<MyArgs, MyResult>() starter.onResult { result !% !!$ } val dialogController = rememberDialogController()
  94. 101 val viewModel = weaverViewModel<MyViewModel>() val subgraph = viewSubgraph<MySubgraph>() val

    starter = rememberContentViewStarter<MyArgs, MyResult>() starter.onResult { result !% !!$ } val dialogController = rememberDialogController() when running on Preview Do not crash
  95. 102 Do not crash

  96. 102 Do not crash

  97. 103 LocalInspectionM

  98. 104 LocalInspectionMode .current = true // when in a preview

  99. 105 LocalInspectionMode Provide defaults if set ent = true //

    when in a preview
  100. 106 internal object HorizonFontFamily { val default: FontFamily @Composable @ReadOnlyComposable

    get() = when { LocalInspectionMode.current !% chirpFontFamily FontFeatures.isChirpEnabled() !% chirpFontFamily else !% FontFamily.Default } private val chirpFontFamily by lazy { !!$ } } Provide defaults if set A sensible default
  101. 107 internal object HorizonFontFamily { val default: FontFamily @Composable @ReadOnlyComposable

    get() = when { LocalInspectionMode.current !% chirpFontFamily FontFeatures.isChirpEnabled() !% chirpFontFamily else !% FontFamily.Default } private val chirpFontFamily by lazy { !!$ } } A sensible default
  102. 108 internal object HorizonFontFamily { val default: FontFamily @Composable @ReadOnlyComposable

    get() = when { LocalInspectionMode.current !% chirpFontFamily FontFeatures.isChirpEnabled() !% chirpFontFamily else !% FontFamily.Default } private val chirpFontFamily by lazy { !!$ } } A sensible default
  103. LiveEdit are pretty picky I want to believe

  104. 110 Feature Parity with Views is no

  105. 111 Feature Parity h Views is not quite there yet

  106. 112 Feature Parity systems quite there yet AndroidView is a

    life saver!
  107. 113 Gotchas

  108. @Composable @UiComposable fun <T : View> AndroidView( factory: (Context) !%

    T, modifier: Modifier = Modifier, update: (T) !% Unit = NoOpUpdate ) { val context = LocalContext.current !# NoOp Connection required by nested scroll modifier. Th !# to influence nested scrolling with it and it is requir 114 AndroidView are pretty picky use Views when necessary
  109. 115 Text for custom emoji TextField for media embeds and

    custom emoji weetView interop with our modular view collection
  110. 116 Text

  111. 117 Created a new composable as a Text facade Text

    https://bit.ly/compose-textview-wrapper
  112. 117 Created a new composable as a Text facade Which

    one do we use? Compose or View? Decide via heuristics Allow forcing the wrapper Our facade share a similar API with Text Use the original as fallback Text https://bit.ly/compose-textview-wrapper
  113. 118 @Composable private fun requiresTextViewRendering( text: String, emojiCompat: EmojiCompat =

    EmojiCompat.get() ): Boolean = remember(text, emojiCompat) { emojiCompat.getLoadState() !" LOAD_STATE_SUCCEEDED !( emojiCompat.getEmojiMatch(input, Integer.MAX_VALUE) } Text heuristics
  114. 119 @Composable private fun requiresTextViewRendering( text: String, emojiCompat: EmojiCompat =

    EmojiCompat.get() ): Boolean = remember(text, emojiCompat) { emojiCompat.getLoadState() !" LOAD_STATE_SUCCEEDED !( emojiCompat.getEmojiMatch(input, Integer.MAX_VALUE) } Text heuristics
  115. 120 @Composable fun Text( text: String, modifier: Modifier = Modifier,

    color: Color = Color.Unspecified, fontSize: TextUnit = TextUnit.Unspecified, fontStyle: FontStyle? = null, fontWeight: FontWeight? = null, fontFamily: FontFamily? = null, letterSpacing: TextUnit = TextUnit.Unspecified, textDecoration: TextDecoration? = null, textAlign: TextAlign? = null, lineHeight: TextUnit = TextUnit.Unspecified, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, Text share a similar API
  116. 122 @Composable fun Text( text: String, Text forceTextView: Boolean =

    false, ) { if (forceTextView !) requiresTextViewRendering(text)) { TextViewWrapper( text = text, factory = !&TypefacesTextView, modifier = modifier, !#!!$ ) } else { androidx.compose.material.Text( text = text, !#!!$ allow forcing the wrapper ) } else { androidx.compose.material.Text( text = text, AndroidView https://bit.ly/compose-textview-wrapper
  117. 123 @Composable fun Text( text: String, Text forceTextView: Boolean =

    false, ) { if (forceTextView !) requiresTextViewRendering(text)) { TextViewWrapper( !#!!$ use the original as fallback } else { androidx.compose.material.Text( text = text, modifier = modifier, !#!!$ ) } !* !!$ !+) }
  118. 124 @Composable fun Text( text: String, Text forceTextView: Boolean =

    false, ) { if (forceTextView !) requiresTextViewRendering(text)) { TextViewWrapper( !#!!$ use the original as fallback } else { androidx.compose.material.Text( text = text, modifier = modifier, !#!!$ ) } !* !!$ !+) }
  119. 124 @Composable fun Text( text: String, Text forceTextView: Boolean =

    false, ) { if (forceTextView !) requiresTextViewRendering(text)) { TextViewWrapper( !#!!$ use the original as fallback } else { androidx.compose.material.Text( text = text, modifier = modifier, !#!!$ ) } !* !!$ !+) } Composable
  120. 4:55PM · May 13, 2021 6 16 46 Declarative programming

    (including Compose) usually takes 3-6 months of usage before people have the ""⚡holly-shit this is good" moment. @JimSproch Jim Sproch AndroidView is a life saver! Until then, you will struggle with it, you will fight it, it will frustrate you, and then it will all click in your mind six months later.
  121. 4:55PM · May 13, 2021 6 16 46 Declarative programming

    (including Compose) usually takes 3-6 months of usage before people have the ""⚡holly-shit this is good" moment. @JimSproch Jim Sproch AndroidView is a life saver! Until then, you will struggle with it, you will fight it, it will frustrate you, and then it will all click in your mind six months later.
  122. Not everybody starts at the same page Onboarding to Compose

  123. Not everybody starts at the same page Onboarding to Compose

    Easy to make sneaky mistakes
  124. 15h of review time per week per engineer Nth time

    you repeat the same feedback for a different person
  125. Nth time you repeat the same feedback for a different

    person h er er
  126. Nth time you repeat the same feedback for a different

    person h er er
  127. Static checks Best ROI for our team Scale really well

    Bikeshedding is time consuming and frustrating for all parts involved Quality error messages that link to the documentation
  128. Static checks

  129. Static checks

  130. Allows custom rules and has autofixes Lint has been breaking

    a lot with AGP updates over the years Really fast, enables us to run it in all our pre-commit git hooks Great IDE plugin (unofficial) Let's see some of our custom rules
  131. Let's see some of our custom rules

  132. @Composable fun MyComposable() { Column { Text("Hi~") } }

  133. @Composable fun MyComposable() { Column { Text("Hi~") } } @Composable

    fun MyComposable(modifier: Modifier = Modifier) { Column(modifier = modifier) { Text("Hi~") } } Always provide a modifier https://chris.banes.dev/always-provide-a-modifier/
  134. @Composable fun MyComposable(modifier: Modifier = Modifier) { Column(modifier = modifier)

    { Text( text = "Hi~", modifier = modifier ) } }
  135. @Composable fun MyComposable(modifier: Modifier = Modifier) { Column(modifier = modifier)

    { Text( text = "Hi~", modifier = modifier ) } } Do not reuse modifiers
  136. @Composable fun MyComposable() { Text("Hi") Text("Hola") }

  137. @Composable fun MyComposable() { Text("Hi") Text("Hola") } @Composable fun MyComposable()

    { Column(!!$) { Text("Hi") Text("Hola") } } @Composable fun ColumnScope.MyComposable() { Text("Hi") Text("Hola") } Don't emit content from >1 sources at the top level
  138. @Composable fun MyComposable(): String { Text("Hi") return remember { "Hola"

    } }
  139. Emit content OR return a result, not both @Composable fun

    MyComposable() { Text("Hi") } @Composable fun rememberMyString(): String { return remember { "Hola" } } @Composable fun MyComposable(): String { Text("Hi") return remember { "Hola" } } Composables that emit co
  140. ult, not both @Composable fun MyComposable() { Text("Hi") } @Composable

    fun rememberMyString(): String { return remember { "Hola" } } @Composable fun MyComposable(): String { Text("Hi") return remember { "Hola" } } Composables that emit content return Unit
  141. @Composable fun MyComposable(list: ArrayList<String>) { !!$ } @Composable fun MyComposable(state:

    MutableState<String>) { !!$ } Don't use mutable types as params in a Composable
  142. @Composable fun MyComposable() { val state by mutableStateOf("Hi") } @Composable

    fun MyComposable() { val state by derivedStateOf { !!$ } }
  143. mutableStateOf and derivedStateOf need to be remembered in a composable

    @Composable fun MyComposable() { val state by remember { mutableStateOf("Hi") } } @Composable fun MyComposable() { val state by remember { derivedStateOf { !!$ } } } @Composable fun MyComposable() { val state by mutableStateOf("Hi") } @Composable fun MyComposable() { val state by derivedStateOf { !!$ } }
  144. @Composable fun MyComposable() { val viewModel by viewModel<MyViewModel>() !# !!$

    }
  145. Inject your ViewModels as default parameters @Composable fun MyComposable( viewModel:

    MyViewModel = viewModel() ) { !# !!$ } @Composable fun MyComposable() { val viewModel by viewModel<MyViewModel>() !# !!$ }
  146. Inject your DI dependencies as default parameters @Composable fun MyComposable(

    ) { !# !!$ } @Composable fun MyComposable() { !# !!$ } ers val myObject = viewSubgraph<MySubgraph>.myObject myObject: MyObject = viewSubgraph<MySubgraph>.myObject
  147. val myObject = viewSubgraph<MySubgraph>.myObject myObject: MyObject = viewSubgraph<MySubgraph>.myObject Implicit dependencies

    should be made explicit in the composable method signature @Composable fun MyComposable( ) { !# !!$ } @Composable fun MyComposable() { !# !!$ } val myObject = anyDependency() myObject: MyObject = anyDependency() ters
  148. @Composable fun MyComposable(viewModel: MyViewModel = viewModel()) { MyOtherComposable(viewModel) } @Composable

    fun MyOtherComposable(viewModel: MyViewModel) { !!$ }
  149. @Composable fun MyComposable(viewModel: MyViewModel = viewModel()) { MyOtherComposable(viewModel) } @Composable

    fun MyOtherComposable(viewModel: MyViewModel) { !!$ }
  150. ViewModels should not be passed around: hoist state and send

    events back via lambdas @Composable fun MyComposable(viewModel: MyViewModel = viewModel()) { MyOtherComposable(viewModel) } @Composable fun MyOtherComposable(viewModel: MyViewModel) { !!$ } @Composable fun MyComposable(viewModel: MyViewModel = viewModel()) { val state by viewModel.state.collectAsState() MyOtherComposable(state) { value !% viewModel.someEvent(value) } }
  151. Static checks We wrote the ktlint rules, based on common

    issues we saw in code reviews We expanded the error messages for the rules and added links to the docs We created our best practices docs based on these custom rules Open source pretty please? Asking for a friend... 7:35PM · Apr 6, 2022 Harold @hidethepain Yep! We are now working on it ʕ/ ·ᴥ·ʔ/ tools, not rules
  152. Testing

  153. Testing We heavily use Writing comprehensive UI tests is tricky

    for us Almost all of our tests are JVM tests And tests? Very little device tests, managed by our Quality Engineering team
  154. Testing Still Screenshot tests for peace of mind For AndroidViews

    we use on top Paparazzi Still JVM tests
  155. Paparazzi https://github.com/cashapp/paparazzi

  156. Paparazzi Doesn't like so we remove it from Paparazzi-enabled modules

    We auto-clip transparency from renders to save space and make code reviews easier We also add borders around components Our Paparazzi composable runs with LocalInspectionMode set to true https://bit.ly/paparazzi-twitter
  157. Using Compose at ! Docs available for all levels Optimize

    @Previews working reliably AndroidView to get feature parity Static checks to scale adoption Unit + screenshot test composables
  158. 162 Benefits, gotchas & mistakes

  159. Benefits of Compose such fast much simpler API wow stateless

    very performance so testing much kotlin so animating
  160. Benefits Working closer with UX Compose gave us a chance

    to reset our working relationship with UX and Product teams We can now iterate on component feedback much faster Creating the components has allowed us to influence the design system
  161. Gotchas Compose is moving fast Takes effort to keep everyone

    up to date Lots of experimental APIs, which are likely to change The separate layers and libraries can be daunting
  162. Gotchas Tied to Kotlin version Compose Compiler is tied to

    the specific version of Kotlin it was built against Recently stuck on Compose 1.1.0-beta01 due to being pinned to Kotlin 1.5.31 Due to various Kotlin / kapt issues
  163. Gotchas Tied to Kotlin version Compose Compiler is tied to

    the specific version of Kotlin it was built against Recently stuck on Compose 1.1.0-beta01 due to being pinned to Kotlin 1.5.31 You can now pin just the Compose Compiler version, and upgrade Compose Runtime → Material as needed
  164. Mistakes Creating wrappers for small components It's very easy to

    create View wrappers for composables using ComposeView We've found that using many ComposeViews tends to scale badly in terms of performance Very easy to include multiple ComposeView instances in performance critical UIs (such as list items)
  165. Mistakes Creating wrappers for small components Be purposeful when choosing

    what composables to wrap Prefer wrapping medium → large pieces of UI, to negate the ‘cost’ of ComposeView For example, don't wrap your Button() composable like we did
  166. Thank You!