$30 off During Our Annual Pro Sale. View Details »

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. Branching
    out to
    Jetpack
    Compose
    Chris Banes
    @chrisbanes
    Nacho López
    @mrmans0n
    !

    View Slide

  2. !

    View Slide

  3. Optional section title
    3

    View Slide

  4. Compose
    is on your
    device

    View Slide

  5. ...but how?

    View Slide

  6. What we did was
    not perfect

    View Slide

  7. What we didn't
    was not perfect
    Your journey will
    be different

    View Slide

  8. 8
    Our journey

    View Slide

  9. Twitter for Android

    View Slide

  10. 1000+
    modules

    View Slide

  11. 300+
    UI modules

    View Slide

  12. 30+
    teams

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  16. 17
    First, we had
    to sort out
    our theming
    ONE DOES NOT SIMPLY
    WRITE A COMPOSE COMPONENT

    View Slide

  17. 18
    Design system
    Doesn't map 1:1 to Material

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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
    Layers which
    contain Material

    View Slide

  22. Most apps with their
    own design system
    will be somewhere in
    the middle
    Material
    0

    View Slide

  23. To maintain platform
    consistency
    Touch highlights
    App chrome
    Elevation
    Material
    0

    View Slide

  24. 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

    View Slide

  25. 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

    View Slide

  26. 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?

    View Slide

  27. 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

    View Slide

  28. 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

    View Slide

  29. 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

    View Slide

  30. Other teams then started using
    Compose, adding to the workload
    EAPs
    Those teams weren't always
    ready to support Compose
    Early access partners

    View Slide

  31. 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

    View Slide

  32. 2021
    mber December How do we
    spend less
    time on
    support?

    View Slide

  33. 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

    View Slide

  34. 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

    View Slide

  35. .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

    View Slide

  36. 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.

    View Slide

  37. 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.

    View Slide

  38. 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?

    View Slide

  39. 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.

    View Slide

  40. 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.

    View Slide

  41. 2022
    June July August Se

    View Slide

  42. Allowlist
    Any team can use Compose

    View Slide

  43. 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

    View Slide

  44. 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

    View Slide

  45. 2022
    October

    View Slide

  46. 90+
    Compose modules

    View Slide

  47. 20+
    Teams using
    Compose

    View Slide

  48. Using
    Compose
    at "

    View Slide

  49. Twitter for
    Android
    First commit in the repo from 2010
    A lot of custom infra built in
    Infra usually created before OSS alternatives

    View Slide

  50. Examples of custom infra
    UI

    View Slide

  51. UI DI
    Examples of custom infra

    View Slide

  52. All our custom infra
    is properly
    val viewModel = weaverViewModel()
    val state by viewModel.watchAsState()
    supported

    View Slide

  53. val viewModel = weaverViewModel()
    val state by viewModel.watchAsState()
    val myObject = viewSubgraph().myObject
    All our custom infra
    is properly supported

    View Slide

  54. +
    All our custom infra
    is properly documented

    View Slide

  55. +
    ...and so on
    All our custom infra
    is properly documented

    View Slide

  56. 72
    documented
    properly

    View Slide

  57. 73

    View Slide

  58. 73

    View Slide

  59. 74
    Documentation
    for all levels

    View Slide

  60. 75
    Codelabs
    are great starter docs

    View Slide

  61. 76
    Codelabs

    View Slide

  62. View Slide

  63. Gotchas
    should be documented too

    View Slide

  64. 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!

    View Slide

  65. Best
    practices

    View Slide

  66. Tools, not rules
    Best
    practices?

    View Slide

  67. Tools, not rules
    Tooling
    We'll get there, but

    View Slide

  68. Tools
    P
    re
    v
    ie
    w
    s
    Infra &

    View Slide

  69. Previews

    View Slide

  70. 87
    Previews
    multiple themes
    from Dolphin+

    View Slide

  71. 88
    Standard Lights
    out
    Dim
    3 themes
    need previews
    Previews

    View Slide

  72. 89
    enum class ThemeVariant(private val themeSuffix: String) {
    }
    STANDARD(".Standard"),
    DIM(".Dim"),
    LIGHTS_OUT(".LightsOut");
    !# !!$

    View Slide

  73. 90
    class ThemeVariantPreviewProvider : PreviewParameterProvider {
    override val values = ThemeVariant.values().asSequence()
    }
    enum class ThemeVariant(private val themeSuffix: String) { }
    !!$

    View Slide

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

    View Slide

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

    View Slide

  76. @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

    View Slide

  77. 93
    @Preview
    @Composable
    fun MyPreview(
    ) {
    val (variant, myData) = model
    HorizonTheme(themeVariant = variant) {
    MyComposable(myData)
    }
    }
    @PreviewParameter( )
    !&class
    MyPreviewProvider model: Pair
    val (variant, myData) = model
    themeVariant = variant
    myData
    https://bit.ly/composite-preview-provider

    View Slide

  78. 94
    Previews
    custom infra run in their own A
    that we don't c

    View Slide

  79. 95
    Previews
    run in their own Activity
    that we don't control

    View Slide

  80. 97
    val viewModel = weaverViewModel()
    val subgraph = viewSubgraph()
    val starter = rememberContentViewStarter()
    starter.onResult { result !% !!$ }
    val dialogController = rememberDialogController()
    when running on Preview
    Do not crash

    View Slide

  81. 98
    Do not crash

    View Slide

  82. 98
    Do not crash

    View Slide

  83. 99
    LocalInspectionM

    View Slide

  84. 100
    LocalInspectionMode
    .current = true // when in a preview

    View Slide

  85. 101
    LocalInspectionMode
    Provide defaults if set
    ent = true // when in a preview

    View Slide

  86. 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

    View Slide

  87. 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

    View Slide

  88. 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

    View Slide

  89. LiveEdit
    are pretty picky
    I want to believe

    View Slide

  90. 106
    Feature Parity
    with Views is no

    View Slide

  91. 107
    Feature Parity
    h Views is not quite there yet

    View Slide

  92. 108
    Feature Parity
    systems
    quite there yet AndroidView is a life saver!

    View Slide

  93. 109
    Gotchas

    View Slide

  94. @Composable
    @UiComposable
    fun 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

    View Slide

  95. 111
    Text
    for custom emoji
    TextField
    for media embeds and custom emoji
    weetView
    interop with our modular view collection

    View Slide

  96. 112
    Text

    View Slide

  97. 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

    View Slide

  98. View Slide

  99. 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.

    View Slide

  100. 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.

    View Slide

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

    View Slide

  102. Not everybody starts at the same page
    Onboarding
    to Compose
    Easy to make sneaky mistakes

    View Slide

  103. 15h
    of review time per
    week per engineer
    Nth
    time you repeat the
    same feedback for a
    different person

    View Slide

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

    View Slide

  105. 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

    View Slide

  106. Static
    checks

    View Slide

  107. 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)

    View Slide

  108. 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.

    View Slide

  109. 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

    View Slide

  110. 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

    View Slide

  111. Static
    checks
    Compose
    Rules
    https://github.com/twitter/compose-rules
    Detekt

    View Slide

  112. Static
    checks
    Compose
    Rules
    https://github.com/twitter/compose-rules
    Detekt

    View Slide

  113. Hoist all the things

    View Slide

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

    View Slide

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

    View Slide

  116. 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

    View Slide

  117. @Composable
    fun MyComposable(list: ArrayList) { !!$ }
    @Composable
    fun MyComposable(state: MutableState) { !!$ }
    Don't use mutable types as params in a
    Composable
    Hoist all the things

    View Slide

  118. Avoid implicit dependencies

    View Slide

  119. @Composable
    fun MyComposable() {
    val viewModel by viewModel()
    !# !!$
    }
    Avoid implicit dependencies

    View Slide

  120. Inject your ViewModels as default parameters
    @Composable
    fun MyComposable(
    viewModel: MyViewModel = viewModel()
    ) {
    !# !!$
    }
    @Composable
    fun MyComposable() {
    val viewModel by viewModel()
    !# !!$
    }
    Avoid implicit dependencies

    View Slide

  121. Inject your DI dependencies as default parameters
    @Composable
    fun MyComposable(
    ) {
    !# !!$
    }
    @Composable
    fun MyComposable() {
    !# !!$
    }
    ers
    val myObject = viewSubgraph.myObject
    myObject: MyObject = viewSubgraph.myObject
    Avoid implicit dependencies

    View Slide

  122. val myObject = viewSubgraph.myObject
    myObject: MyObject = viewSubgraph.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

    View Slide

  123. val LocalBanana = staticCompositionLocalOf { & }
    val LocalApple = compositionLocalOf { ' }
    New CompositionLocals should be avoided
    * unless they are necessary for app-wide infra
    Avoid implicit dependencies

    View Slide

  124. ( needless recompositions

    View Slide

  125. @Composable
    fun UsersList(users: List) {
    LazyColumn { items(users) { user !% User(user) } }
    }
    ( needless recompositions

    View Slide

  126. ( needless recompositions
    @Composable
    fun UsersList(users: List) {
    LazyColumn { items(users) { user !% User(user) } }
    }

    View Slide

  127. @Composable
    fun UsersList(users: List) {
    LazyColumn { items(users) { user !% User(user) } }
    }
    Use Kotlinx Immutable Collections
    * or Immutable wrappers
    ( needless recompositions
    @Composable
    fun UsersList(users: ImmutableList) { !!$ }
    (users: UserList) { !!$ }
    !' or… !(
    @Immutable
    data class UserList(val items: List)

    View Slide

  128. @Composable
    fun MyComposable(viewModel: MyViewModel = viewModel()) {
    OtherComposable(!!$) { viewModel.doSomething(it) }
    }
    ( needless recompositions

    View Slide

  129. @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/

    View Slide

  130. Modifiers for the win

    View Slide

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

    View Slide

  132. @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/

    View Slide

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

    View Slide

  134. @Composable
    fun MyComposable(modifier: Modifier = Modifier) {
    Column(modifier = modifier) {
    Text(
    text = "Hi~",
    modifier = modifier
    )
    }
    }
    Do not reuse modifiers

    View Slide

  135. 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

    View Slide

  136. Testing

    View Slide

  137. 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

    View Slide

  138. Testing
    Still
    Screenshot tests for peace of mind
    For AndroidViews we use on top
    Paparazzi
    Still JVM tests

    View Slide

  139. Paparazzi
    https://github.com/cashapp/paparazzi

    View Slide

  140. 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

    View Slide

  141. 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

    View Slide

  142. 155
    Benefits,
    gotchas &
    mistakes

    View Slide

  143. Benefits
    such fast
    much simpler API
    wow stateless
    very performance
    so testing
    much kotlin

    View Slide

  144. 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

    View Slide

  145. 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

    View Slide

  146. 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

    View Slide

  147. 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

    View Slide

  148. 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)

    View Slide

  149. 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

    View Slide

  150. Thank You!

    View Slide