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

Composing an Octopus

Composing an Octopus

At TIER, we’re in the process of adopting Jetpack Compose into our quite large code base. One of the most significant milestones of this journey was to have our design system, Octopus, implemented in pure Compose UI. In this talk, I’ll present how we built Octopus Compose, and what we gained by reimplementing components instead of wrapping existing Views in AndroidView.

I gave this talk on the 2022 November edition of the Android Budapest Meetup on 2022.12.01.

István Juhos

December 01, 2022
Tweet

More Decks by István Juhos

Other Decks in Programming

Transcript

  1. @stewemetal 👨💻 Senior Android Engineer @ TIER 
  Co-organizer

    of Kotlin Budapest István Juhos Composing an Octopus 🐙
  2. @stewemetal Octopus Design System - B.C. • 100% Views with

    Data Binding and @BindingAdapters • Good component APIs & docs • Easy usage with View-based UI
  3. @stewemetal Octopus Design System - B.C. • 100% Views with

    Data Binding and @BindingAdapters • Good component APIs & docs • Easy usage with View-based UI • Not fun to maintain 😢 OctopusButtonPrimary.kt R.layout.__internal_view_octopus_button R.styleable.OctopusButton R.drawable.__internal_octopus_button_background_primary @stewemetal
  4. @stewemetal Octopus Design System - B.C. • 100% Views with

    Data Binding and @BindingAdapters • Good component APIs & docs • Easy usage with View-based UI • Not fun to maintain 😢 // We have to disable specific setters to prevent misuse of the view private var areSettersEnabled = false @stewemetal
  5. @stewemetal Octopus Design System - B.C. // Disable the setters

    if it's not an internal call /** * Calling this method will have no effect. */ override fun setTextColor(color: Int) { if (areSettersEnabled || isInEditMode) { super.setTextColor(color) } }
  6. @stewemetal Octopus Design System - B.C. // This is a

    known bug in Android Studio Design Editor // https://issuetracker.google.com/issues/36942641 if (isInEditMode) { setTypeface( typeface, if (textType.isBold) Typeface.BOLD else Typeface.NORMAL, ) } else { val fontRes = if (textType.isBold) R.font.tier_text_bold else R.font.tier_text_regular typeface = ResourcesCompat.getFont(context, fontRes) }
  7. @stewemetal Octopus Design System • Possible approaches for us Views

    + AndroidView @Composable fun OctopusButtonPrimary( text: String, … onClick: () -> Unit, ) { AndroidView( factory = { context -> OctopusButtonPrimary(context) }, update = { view -> ... }, ) } @stewemetal
  8. @stewemetal Octopus Design System • Possible approaches for us Views

    + AndroidView @Composable fun OctopusButtonPrimary( text: String, … onClick: () -> Unit, ) { AndroidView( factory = { context -> OctopusButtonPrimary(context) }, update = { view -> ... }, ) } @stewemetal
  9. @stewemetal Octopus Design System • Possible approaches for us Views

    + AndroidView @Composable fun OctopusButtonPrimary( text: String, … onClick: () -> Unit, ) { AndroidView( factory = { context -> OctopusButtonPrimary( ContextThemeWrapper( context, R.style.Theme_Octopus, ) ) }, update = { view -> ... }, ) } @stewemetal
  10. @stewemetal Octopus Design System • Possible approaches for us Views

    + AndroidView @Composable fun OctopusButtonPrimary( text: String, … onClick: () -> Unit, ) { AndroidView( factory = { context -> OctopusButtonPrimary( ContextThemeWrapper( context, R.style.Theme_Octopus, ) ).apply { setText(text) setOnClickListener { onClick() } ... } }, @stewemetal
  11. @stewemetal Octopus Design System • Possible approaches for us Views

    + AndroidView + XMLs @Composable fun OctopusButtonPrimary( text: String, … onClick: () -> Unit, ) { AndroidView( factory = { context -> OctopusButtonPrimary( ContextThemeWrapper( context, R.style.Theme_Octopus, ) ).apply { setText(text) setOnClickListener { onClick() } ... } }, @stewemetal
  12. @stewemetal Octopus Design System • Possible approaches for us @Composable

    fun OctopusButtonPrimary( text: String, … onClick: () -> Unit, ) { AndroidView( factory = { context -> OctopusButtonPrimary( ContextThemeWrapper( context, R.style.Theme_Octopus, ) ).apply { setText(text) setOnClickListener { onClick() } ... } }, @stewemetal Views + AndroidView + XMLs
  13. @stewemetal Octopus Design System • Possible approaches for us @Composable

    public fun OctopusButtonPrimary( text: String, modifier: Modifier = Modifier, buttonSize: ButtonSize = NORMAL, enabled: Boolean = true, loading: Boolean = false, onClick: () -> Unit, ) { Box() { OctopusRippleTheme() { Button() { Text() } if (loading) { OctopusButtonLoader() } } } } Reimplement component s​ in ​ Compose @stewemetal
  14. @stewemetal Octopus Design System • Possible approaches for us @Composable

    public fun OctopusButtonPrimary( text: String, modifier: Modifier = Modifier, buttonSize: ButtonSize = NORMAL, enabled: Boolean = true, loading: Boolean = false, onClick: () -> Unit, ) { Box() { OctopusRippleTheme() { Button() { Text() } if (loading) { OctopusButtonLoader() } } } } Reimplement component s​ in ​ Compose @stewemetal
  15. @stewemetal Octopus Design System • Possible approaches for us Reimplement

    component s​ in ​ Compose @stewemetal @Composable public fun OctopusButtonPrimary( text: String, modifier: Modifier = Modifier, buttonSize: ButtonSize = NORMAL, enabled: Boolean = true, loading: Boolean = false, onClick: () -> Unit, ) { Box() { OctopusRippleTheme() { Button() { Text() } if (loading) { OctopusButtonLoader() } } } }
  16. @stewemetal Octopus Design System • Possible approaches for us Reimplement

    component s​ in ​ Compose @stewemetal @Composable public fun OctopusButtonPrimary( text: String, modifier: Modifier = Modifier, buttonSize: ButtonSize = NORMAL, enabled: Boolean = true, loading: Boolean = false, onClick: () -> Unit, ) { Box() { OctopusRippleTheme() { Button() { Text() } if (loading) { OctopusButtonLoader() } } } }
  17. @stewemetal • The benefits of pure Compose • Easier to

    maintain • No attachments to the legacy Views • Easy for teams to adopt Octopus Design System 🤩
  18. @stewemetal • Downsides of our approach • A second implementation

    to maintain • Needs parity with the View components • Some APIs are still experimental Octopus Design System 🧐
  19. @stewemetal Octopus Design System • Three possible approaches • Customize

    Material • Extend Material • Go fully-custom - but use Material components
  20. @stewemetal @Composable public fun OctopusTheme( isInDarkMode: Boolean = isSystemInDarkTheme(), colors:

    OctopusColors = OctopusColors.build(isInDarkMode), typography: OctopusTypography = OctopusTypography.build(), shapes: OctopusShapes = OctopusShapes.build(), dimensions: OctopusDimensions = OctopusDimensions.build(), spacings: OctopusSpacings = OctopusSpacings(), content: @Composable () -> Unit, ) { content() } OctopusTheme.kt
  21. @stewemetal @Composable public fun OctopusTheme( isInDarkMode: Boolean = isSystemInDarkTheme(), colors:

    OctopusColors = OctopusColors.build(isInDarkMode), ... content: @Composable () -> Unit, ) { CompositionLocalProvider( LocalColors provides colors, ... ) { content() } } OctopusTheme.kt
  22. @stewemetal @Composable public fun OctopusTheme( isInDarkMode: Boolean = isSystemInDarkTheme(), colors:

    OctopusColors = OctopusColors.build(isInDarkMode), ... content: @Composable () -> Unit, ) { CompositionLocalProvider( LocalColors provides colors, ... ) { content() } } OctopusTheme.kt
  23. @stewemetal private val LocalColors = compositionLocalOf<OctopusColors> { error( 
 "No

    colors provided! Make sure to wrap all Octopus components 
 in an OctopusTheme." 
 ) } OctopusTheme.kt
  24. @stewemetal public object OctopusTheme { public val colors: OctopusColors @Composable

    @ReadOnlyComposable get() = LocalColors.current ... } OctopusTheme.kt
  25. @stewemetal OctopusColors.kt data class OctopusColors( public val primaryColors: PrimaryColors, public

    val secondaryColors: SecondaryColors, public val functionalColors: FunctionalColors, public val neutralColors: NeutralColors, public val semanticColors: SemanticColors, ... )
  26. @stewemetal BasicColors.kt internal val green = Color(0xff067a72) internal val vehicleGreen

    = Color(0xff30e0cc) internal val blue = Color(0xff0e1a50) ... data class PrimaryColors internal constructor( public val green: Color = green, public val vehicleGreen: Color = vehicleGreen, public val blue: Color = blue, ... )
  27. @stewemetal • Have clean APIs • Only allow customization defined

    in the specs • Use values provided by OctopusTheme • Build on existing Material components Designing Components in Compose
  28. @stewemetal • Have clean APIs • Only allow customization defined

    in the specs • Use values provided by OctopusTheme • Build on existing Material components (if possible) Designing Components in Compose
  29. @stewemetal OctopusText.kt @Composable public fun OctopusText( text: String, modifier: Modifier

    = Modifier, textType: TextType = BODY_1, links: List<Link> = emptyList(), textAlignment: Alignment = NATURAL, overflow: TextOverflow = TextOverflow.Clip, style: OctopusStyle = OctopusStyle.getDefault(), ) { ... }
  30. @stewemetal OctopusText.kt @Composable public fun OctopusText( text: String, modifier: Modifier

    = Modifier, textType: TextType = BODY_1, links: List<Link> = emptyList(), textAlignment: Alignment = NATURAL, overflow: TextOverflow = TextOverflow.Clip, style: OctopusStyle = OctopusStyle.getDefault(), ) { ... }
  31. @stewemetal TextType.kt enum class TextType( private val id: Int, @ColorRes

    public val textColorRes: Int, public val textSizeSp: Float, public val lineHeightSp: Float, public val isBold: Boolean = true, public val maxLines: Int = Int.MAX_VALUE, public val isAllCaps: Boolean = false, ) { NUMBERS_1(0, R.color.__internal_octopusTextNumber, TEXT_SIZE_38, 
 LINE_HEIGHT_38, maxLines = 1), NUMBERS_2(1, R.color.__internal_octopusTextNumber, TEXT_SIZE_28, 
 LINE_HEIGHT_28, maxLines = 1), ... }
  32. @stewemetal OctopusText.kt @Composable public fun OctopusText( text: String, modifier: Modifier

    = Modifier, textType: TextType = BODY_1, links: List<Link> = emptyList(), textAlignment: Alignment = NATURAL, overflow: TextOverflow = TextOverflow.Clip, style: OctopusStyle = OctopusStyle.getDefault(), ) { ... }
  33. @stewemetal OctopusText.kt @Composable public fun OctopusText( text: String, modifier: Modifier

    = Modifier, textType: TextType = BODY_1, links: List<Link> = emptyList(), textAlignment: Alignment = NATURAL, overflow: TextOverflow = TextOverflow.Clip, style: OctopusStyle = OctopusStyle.getDefault(), ) { ... }
  34. @stewemetal OctopusText.kt @Composable public fun OctopusText( ... textType: TextType =

    BODY_1, ... ) { ProvideTextStyle( value = OctopusTheme.typography.run { when (textType) { NUMBERS_1 -> text.numbers1 NUMBERS_2 -> text.numbers2 ... } }, ) { ... } }
  35. @stewemetal OctopusText.kt @Composable public fun OctopusText( ... textType: TextType =

    BODY_1, ... ) { ProvideTextStyle( value = OctopusTheme.typography.run { when (textType) { NUMBERS_1 -> text.numbers1 NUMBERS_2 -> text.numbers2 ... } }, ) { ... } }
  36. @stewemetal OctopusText.kt @Composable public fun OctopusText( ... textType: TextType =

    BODY_1, ... ) { ProvideTextStyle( value = OctopusTheme.typography.run { when (textType) { NUMBERS_1 -> text.numbers1 NUMBERS_2 -> text.numbers2 ... } }, ) { ... } }
  37. @stewemetal OctopusText.kt @Composable public fun OctopusText( ... textType: TextType =

    BODY_1, ... ) { ProvideTextStyle(...) { Text( text = if (textType.isAllCaps) text.uppercase() else text, overflow = overflow, modifier = modifier, maxLines = textType.maxLines, // The rest is coming from LocalTextStyle.current 🤩 ) } }
  38. @stewemetal OctopusPreviews @Preview( name = "1 - Light", showBackground =

    true, backgroundColor = 0xffffffff, // primaryBackgroundLight group = "Light/Dark", ) @Preview( name = "2 - Dark", showBackground = true, uiMode = UI_MODE_NIGHT_YES, backgroundColor = 0xff060a1e, // primaryBackgroundDark group = "Light/Dark", ) internal annotation class OctopusPreviewPrimaryBackground
  39. @stewemetal OctopusPreviews @OctopusPreviewPrimaryBackground @Composable private fun OctopusTextPreview() { Column( modifier

    = Modifier.verticalScroll(rememberScrollState()), ) { OctopusTextComposeDemo() } }
  40. @stewemetal OctopusPreviews @OctopusPreviewPrimaryBackground @Composable private fun OctopusTextPreview() { Column( modifier

    = Modifier.verticalScroll(rememberScrollState()), ) { OctopusTextComposeDemo() } }
  41. @stewemetal OctopusPreviews @OctopusPreviewPrimaryBackground @Composable private fun OctopusTextPreview() { Column( modifier

    = Modifier.verticalScroll(rememberScrollState()), ) { OctopusTextComposeDemo() } }
  42. @stewemetal OctopusPreviews @OctopusPreviewPrimaryBackground @Composable private fun OctopusTextPreview() { Column( modifier

    = Modifier.verticalScroll(rememberScrollState()), ) { OctopusTextComposeDemo() } }
  43. @stewemetal OctopusPreviews @Composable public fun OctopusTextComposeDemo() { OctopusTheme { Column(...)

    { OctopusComposeDemoSubSection( title = "Numbers", ) { OctopusText( text = "Numbers 1", textType = NUMBERS_1, ) OctopusText( text = "Numbers 1 with long text...”, textType = NUMBERS_1, ) OctopusText(
  44. @stewemetal OctopusPreviews @Composable public fun OctopusTextComposeDemo() { OctopusTheme { Column(...)

    { OctopusComposeDemoSubSection( title = "Numbers", ) { OctopusText( text = "Numbers 1", textType = NUMBERS_1, ) OctopusText( text = "Numbers 1 with long text...”, textType = NUMBERS_1, ) OctopusText(
  45. @stewemetal Going Compose-first • Compose implementation behind the Views -

    ComposeView • Life with minimal XML, and more Kotlin ❤ • Eventually: Compose-only 😎 ⏩
  46. @stewemetal • View wrappers around the Compose components • Custom

    attributes • Tooling attributes • UI Testing 🧐 Going Compose-first
  47. @stewemetal Conclusions • Having a design system to convert to

    Compose is already a win • Choose a path that makes sense for your project and timeline • Consider interoperability • Reuse what you can in Compose • Zero ➡ Hero takes time We’re still here 😄
  48. @stewemetal Composing an Octopus 🐙 István Juhos 👨💻 Senior Android

    Engineer @ TIER 
  Co-organizer of Kotlin Budapest 
 Mastodon: androiddev.social/@stewemetal 
 Twitter: @stewemetal 
 Github: stewemetal 
 Thank you!