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.

Chris Banes

June 03, 2022
Tweet

More Decks by Chris Banes

Other Decks in Programming

Transcript

  1. !

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

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

    DOES NOT SIMPLY WRITE A COMPOSE COMPONENT
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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?
  10. 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
  11. 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
  12. 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
  13. Other teams then started using Compose, adding to the workload

    EAPs Those teams weren't always ready to support Compose Early access partners
  14. Other non-EAP teams then started using Compose, adding to the

    workload EAPs Those teams weren't always ready to support Compose Early access partners
  15. List of modules which are 'allowed' to use Compose Allowlist

    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
  16. 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
  17. .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 bit.ly/compose-allowlist
  18. Allowlist 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.
  19. Allowlist 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.
  20. Allowlist 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?
  21. Allowlist 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.
  22. Allowlist 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.
  23. 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-progress Shepherds
  24. A group of engineers there to 'shepherd' Compose adoption Goal:

    Keep expanding shepherds group from across the company Currently comprised from 5 teams Automatically added to all Compose code reviews W ork-in-progress Shepherds
  25. Twitter for Android First commit in the repo from 2010

    A lot of custom infra built in Infra usually created before OSS alternatives
  26. val viewModel = weaverViewModel<MyViewModel>() val state by viewModel.watchAsState() val myObject

    = viewSubgraph<MySubgraph>().myObject All our custom infra is properly supported
  27. 73

  28. 73

  29. 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!
  30. 90 class ThemeVariantPreviewProvider : PreviewParameterProvider<ThemeVariant> { override val values =

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

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

    { MyComposable() } } @PreviewParameter(ThemeVariantPreviewProvider!&class) variant: ThemeVariant class ThemeVariantPreviewProvider : PreviewParameterProvider<ThemeVariant> { override val values = ThemeVariant.values().asSequence() }
  33. @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
  34. 93 @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
  35. 97 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
  36. 102 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
  37. 103 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
  38. 104 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
  39. @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 110 AndroidView are pretty picky use Views when necessary
  40. 111 Text for custom emoji TextField for media embeds and

    custom emoji weetView interop with our modular view collection
  41. 113 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
  42. 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.
  43. 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.
  44. 15h of review time per week per engineer Nth time

    you repeat the same feedback for a different person
  45. 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
  46. Static checks Allows custom rules and has autofixes Lint has

    been breaking a lot with AGP updates over the years Really fast, runs in all our pre-commit git hooks Great IDE plugin (unofficial)
  47. Static checks 5:15PM · Mar 25, 2022 19 164 638

    A big challenge to face when a big team with a large codebase starts adopting Compose is that not everybody will start at the same page. This happened to as at Twitter. @mrmans0n Nacho López $ Compose is %, allows for amazing things but has a bunch of footguns to be aware of.
  48. Static checks 5:15PM · Mar 25, 2022 19 164 638

    A big challenge to face when a big team with a large codebase starts adopting Compose is that not everybody will start at the same page. This happened to as at Twitter. @mrmans0n Nacho López $ Compose is %, allows for amazing things but has a bunch of footguns to be aware of. Open source pretty please? Asking for a friend... 7:35PM · Mar 25, 2022 Harold @hidethepain
  49. Static checks 5:15PM · Mar 25, 2022 19 164 638

    A big challenge to face when a big team with a large codebase starts adopting Compose is that not everybody will start at the same page. This happened to as at Twitter. @mrmans0n Nacho López $ Compose is %, allows for amazing things but has a bunch of footguns to be aware of. Open source pretty please? Asking for a friend... 7:35PM · Mar 25, 2022 Harold @hidethepain Y U NO DETEKT?!?1?1!?! 6:53PM · Mar 25, 2022 Android 4 lyf @hardcore_engineer
  50. @Composable fun MyComposable(viewModel: MyViewModel = viewModel()) { MyOtherComposable(viewModel) } @Composable

    fun MyOtherComposable(viewModel: MyViewModel) { !!$ } Hoist all the things
  51. @Composable fun MyComposable(viewModel: MyViewModel = viewModel()) { MyOtherComposable(viewModel) } @Composable

    fun MyOtherComposable(viewModel: MyViewModel) { !!$ } Hoist all the things
  52. 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) } } Hoist all the things
  53. @Composable fun MyComposable(list: ArrayList<String>) { !!$ } @Composable fun MyComposable(state:

    MutableState<String>) { !!$ } Don't use mutable types as params in a Composable Hoist all the things
  54. Inject your ViewModels as default parameters @Composable fun MyComposable( viewModel:

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

    ) { !# !!$ } @Composable fun MyComposable() { !# !!$ } ers val myObject = viewSubgraph<MySubgraph>.myObject myObject: MyObject = viewSubgraph<MySubgraph>.myObject Avoid implicit dependencies
  56. 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 Avoid implicit dependencies
  57. val LocalBanana = staticCompositionLocalOf<Banana> { & } val LocalApple =

    compositionLocalOf<Apple> { ' } New CompositionLocals should be avoided * unless they are necessary for app-wide infra Avoid implicit dependencies
  58. @Composable fun UsersList(users: List<TwitterUser>) { LazyColumn { items(users) { user

    !% User(user) } } } Use Kotlinx Immutable Collections * or Immutable wrappers ( needless recompositions @Composable fun UsersList(users: ImmutableList<TwitterUser>) { !!$ } (users: UserList) { !!$ } !' or… !( @Immutable data class UserList(val items: List<TwitterUser>)
  59. @Composable fun MyComposable(viewModel: MyViewModel = viewModel()) { OtherComposable(!!$) { viewModel.doSomething(it)

    } } ( needless recompositions @Composable fun MyComposable(viewModel: MyViewModel = viewModel()) { OtherComposable(!!$, viewModel!&doSomething) } If possible, use method references instead of capturing lambdas https://multithreaded.stitchfix.com/blog/2022/08/05/jetpack-compose-recomposition/
  60. @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/
  61. @Composable fun MyComposable(modifier: Modifier = Modifier) { Column(modifier = modifier)

    { Text( text = "Hi~", modifier = modifier ) } } Do not reuse modifiers
  62. Static checks We wrote static checks, 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 ) tools, not rules There are a couple internal rules still in our codebase, mostly around our internal MVI, DI and design systems use cases github.com/twitter/compose-rules
  63. 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
  64. 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
  65. 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
  66. 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
  67. 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
  68. Gotchas Tied to Kotlin version Compose Compiler is tied to

    the specific version of Kotlin it was built against Recently stuck on an old version Compose due to being pinned to an old Kotlin version Due to various Kotlin / kapt issues
  69. Gotchas Tied to Kotlin version Compose Compiler is tied to

    the specific version of Kotlin it was built against Recently stuck on an old version Compose due to being pinned to an old Kotlin version You can pin just the Compose Compiler version, and upgrade Compose Runtime → Material as needed
  70. 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)
  71. 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