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

Composing a Design System

Anton Shilov
February 24, 2021

Composing a Design System

Jetpack Compose - the promise of bringing our app to life with dramatically less code, interactive tools, and intuitive Kotlin APIs. how would we be able to migrate the existing design system to Jetpack Compose? What are the key steps to follow for successful integration with the existing codebase?

Anton Shilov

February 24, 2021
Tweet

Other Decks in Programming

Transcript

  1. Private and con fi dential ~1 M LoC ~500 0

    UI-tests ~10 0 UI-components 3
  2. Private and con fi dential 1. Design syste m •

    Token s • Component s • Pattern s 2. Android View interoperabilit y 3. Testin g 4. Integration 4
  3. Private and con fi dential 6 @Composable fun HelloCompose() {

    Column{ Text(text = "Hi there!") Text(text = "Welcome to the talk") Button(onClick = {}) { Text(text = "Let's start!") } } }
  4. Private and con fi dential 7 @Composable fun HelloCompose() {

    Column{ Text(text = "Hi there!") Text(text = "Welcome to the talk") Button(onClick = {}) { Text(text = "Let's start!") } } }
  5. Private and con fi dential 8 @Composable fun HelloCompose() {

    Column{ Text(text = "Hi there!") Text(text = "Welcome to the talk") Button(onClick = {}) { Text(text = "Let's start!") } } }
  6. Private and con fi dential 9 @Composable fun HelloCompose() {

    Column{ Text(text = "Hi there!") Text(text = "Welcome to the talk") Button(onClick = {}) { Text(text = "Let's start!") } } }
  7. Private and con fi dential • Future of Android U

    I • Faster developmen t • Easy integration with our architecture — MV I • Our own declarative UI framewor k • Easy to implement design systems Why Compose? 10
  8. Private and con fi dential • How to create a

    design system ? • How to integrate with the current UI ? • How to test ? • How to integrate within the business logic? Questions 12
  9. Private and con fi dential Design system — set of

    rules and tools for UI design 14
  10. Private and con fi dential 21 MatchPhotos Text - H1

    Button - Filled - White Text - P1 CtaBox ic badge-feature-match
  11. Private and con fi dential data class TextStyle( val color:

    Color = Color.Unspecified, val fontSize: TextUnit = TextUnit.Inherit, val fontWeight: FontWeight? = null, val fontStyle: FontStyle? = null, val fontSynthesis: FontSynthesis? = null, val fontFamily: FontFamily? = null, val fontFeatureSettings: String? = null, val letterSpacing: TextUnit = TextUnit.Inherit, val baselineShift: BaselineShift? = null, val textGeometricTransform: TextGeometricTransform? = null, val localeList: LocaleList? = null, val background: Color = Color.Unspecified, val textDecoration: TextDecoration? = null, val shadow: Shadow? = null, val textAlign: TextAlign? = null, val textDirection: TextDirection? = null, val lineHeight: TextUnit = TextUnit.Inherit, val textIndent: TextIndent? = null ) 22
  12. Private and con fi dential 23 H1 = TextStyle( fontFamily

    = FontFamily.Default, fontWeight = FontWeight.Medium, fontSize = 24.sp, lineHeight = 28.sp )
  13. Private and con fi dential 24 data class Typography( val

    H1: TextStyle, val H2: TextStyle, val Action: TextStyle, val Title: TextStyle, val P1: TextStyle, val P2: TextStyle, val P3: TextStyle )
  14. Private and con fi dential 26 data class Palette( val

    primary: Color, val matchPhotosBorder: Color, //. . . )
  15. Private and con fi dential 27 val sizeDp = 16.dp

    val sizeSp = 16.sp dimensionResource(id = R.dimen.size_lg)
  16. Private and con fi dential 29 data class DimensionsTokens( val

    tokenMatchPhotosPhotoSize: Dp, val tokenButtonHeight: Dp, val tokenButtonIconSize: Dp, val tokenButtonIconTextSpacing: Dp, val tokenButtonDisabledOpacity: Float, val tokenButtonStrokeBorderWidth: Dp, val tokenMatchPhotosBorderWidth: Dp, //. . . )
  17. Private and con fi dential 32 val Icons.Filled.TestVector: ImageVector get()

    = materialIcon(name = "Filled.TestVector") { materialPath(fillAlpha = 0.8f) { moveTo(20.0f, 10.0f) lineToRelative(0.0f, 10.0f) lineToRelative(-10.0f, 0.0f) close() } group { materialPath(pathFillType = EvenOdd) { moveTo(0.0f, 10.0f) lineToRelative(-10.0f, 0.0f) close() } } }
  18. Private and con fi dential 35 App Scaffol d Toolba

    r Tex t Conten t Hear t App Scaffol d Toolba r Tex t Conten t Hear t
  19. Private and con fi dential 36 val LocalHeartColor = compositionLocalOf<Color>()

    @Composable fun HeartsScreen() { Providers( LocalHeartColor provides Color.Red ) { LocalHeartColor.current // I can access to the color her } }
  20. Private and con fi dential 37 val LocalHeartColor = compositionLocalOf<Color>()

    @Composable fun HeartsScreen() { Providers( LocalHeartColor provides Color.Red ) { LocalHeartColor.current // I can access to the color her } }
  21. Private and con fi dential 38 val LocalHeartColor = compositionLocalOf<Color>()

    @Composable fun HeartsScreen() { Providers( LocalHeartColor provides Color.Red ) { LocalHeartColor.current // I can access to the color her } }
  22. Private and con fi dential 39 val LocalHeartColor = compositionLocalOf<Color>()

    @Composable fun HeartsScreen() { Providers( LocalHeartColor provides Color.Red ) { LocalHeartColor.current // I can access to the color her } }
  23. Private and con fi dential 40 Key Value LocalHeartColor Color.Red

    LocalContext Context LocalLifecycleOwner LifecycleOwner
  24. Private and con fi dential 41 val LocalTypography = staticCompositionLocalOf<Typography>()

    @Composable val TextStyles: Typography get() = LocalTypography.current TextStyles.H1 // LocalTypography.current.H1
  25. Private and con fi dential 42 val LocalTypography = staticCompositionLocalOf<Typography>()

    @Composable val TextStyles: Typography get() = LocalTypography.current TextStyles.H1 // LocalTypography.current.H1
  26. Private and con fi dential 43 val LocalTypography = staticCompositionLocalOf<Typography>()

    @Composable val TextStyles: Typography get() = LocalTypography.current TextStyles.H1 // LocalTypography.current.H1
  27. Private and con fi dential 44 val LocalTypography = staticCompositionLocalOf<Typography>()

    @Composable val TextStyles: Typography get() = LocalTypography.current TextStyles.H1 // LocalTypography.current.H1
  28. Private and con fi dential 45 @Composable fun Theme( typography:

    Typography, colors: Palette, dimensions: DimensionsTokens, content: @Composable () -> Unit ) { Providers( LocalTypography provides typography, LocalColors provides colors, LocalDimensions provides dimensions, ) { content() } }
  29. Private and con fi dential 46 @Composable fun Theme( typography:

    Typography, colors: Palette, dimensions: DimensionsTokens, content: @Composable () -> Unit ) { Providers( LocalTypography provides typography, LocalColors provides colors, LocalDimensions provides dimensions, ) { content() } }
  30. Private and con fi dential 47 @Composable fun Theme( typography:

    Typography, colors: Palette, dimensions: DimensionsTokens, content: @Composable () -> Unit ) { Providers( LocalTypography provides typography, LocalColors provides colors, LocalDimensions provides dimensions, ) { content() // TextStyles.H1 } }
  31. Private and con fi dential 1. Design syste m •

    Token s • Component s • Pattern s 2. Android View intero p 3. Testin g 4. Integration 49
  32. Private and con fi dential 52 @Composable fun MaterialButton() {

    Button( onClick = {}, content = { Text(text = "Hello Compose”) } ) }
  33. Private and con fi dential 53 @Composable fun SlotApi() {

    Button( onClick = {}, backgroundColor = Color.Red ) { Button( onClick = {}, backgroundColor = Color.Green ) { Button( onClick = {}, backgroundColor = Color.Blue ) { Text(text = "We need to go deeper") } } } }
  34. Private and con fi dential 56 @Composable fun Button( onClick:

    () -> Unit, text: String, modifier: Modifier = Modifier, color: Color = Colors.primary, contentColor: Color = Color.White, icon: ImageVector? = null, enabled: Boolean = true )
  35. Private and con fi dential 57 @Composable fun Button( onClick:

    () -> Unit, text: String, modifier: Modifier = Modifier, color: Color = Colors.primary, contentColor: Color = Color.White, icon: ImageVector? = null, enabled: Boolean = true )
  36. Private and con fi dential 58 @Composable fun Button( onClick:

    () -> Unit, text: String, modifier: Modifier = Modifier, color: Color = Colors.primary, contentColor: Color = Color.White, icon: ImageVector? = null, enabled: Boolean = true )
  37. Private and con fi dential 59 @Composable fun Button( onClick:

    () -> Unit, text: String, modifier: Modifier = Modifier, color: Color = Colors.primary, contentColor: Color = Color.White, icon: ImageVector? = null, enabled: Boolean = true )
  38. Private and con fi dential 60 @Composable private fun ButtonSkeleton(

    onClick: () -> Unit, text: String, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = RoundedCornerShape(Dimensions.tokenButtonCornerRadius), border: BorderStroke? = null, backgroundColor: Color, contentColor: Color = Color.White, contentPadding: PaddingValues = PaddingValues( start = Dimensions.tokenButtonPaddingHorizontal, end = Dimensions.tokenButtonPaddingHorizontal, top = Dimensions.tokenButtonPaddingVertical, bottom = Dimensions.tokenButtonPaddingVertical ), height: Dp = Dimensions.tokenButtonHeight, icon: ImageVector?, )
  39. Private and con fi dential 61 @Composable private fun ButtonSkeleton(

    onClick: () -> Unit, text: String, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = RoundedCornerShape(Dimensions.tokenButtonCornerRadius), border: BorderStroke? = null, backgroundColor: Color, contentColor: Color = Color.White, contentPadding: PaddingValues = PaddingValues( start = Dimensions.tokenButtonPaddingHorizontal, end = Dimensions.tokenButtonPaddingHorizontal, top = Dimensions.tokenButtonPaddingVertical, bottom = Dimensions.tokenButtonPaddingVertical ), height: Dp = Dimensions.tokenButtonHeight, icon: ImageVector? )
  40. Private and con fi dential 64 FilledButton ButtonSkeleto n Surfac

    e Ro w Ico n Tex t - private generic button
  41. Private and con fi dential 68 Surface( shape = shape,

    color = backgroundColor, contentColor = contentColor, border = border, modifier = modifier.clickable( onClick = onClick, enabled = enabled ).alpha( if (enabled) 1f else Dimensions.tokenButtonDisabledOpacity ) )
  42. Private and con fi dential 69 Surface( shape = shape,

    color = backgroundColor, contentColor = contentColor, border = border, modifier = modifier.clickable( onClick = onClick, enabled = enabled ).alpha( if (enabled) 1f else Dimensions.tokenButtonDisabledOpacity ) )
  43. Private and con fi dential 70 Surface( shape = shape,

    color = backgroundColor, contentColor = contentColor, border = border, modifier = modifier.clickable( onClick = onClick, enabled = enabled ).alpha( if (enabled) 1f else Dimensions.tokenButtonDisabledOpacity ) )
  44. Private and con fi dential 71 Surface( shape = shape,

    color = backgroundColor, contentColor = contentColor, border = border, modifier = modifier.clickable( onClick = onClick, enabled = enabled ).alpha( if (enabled) 1f else Dimensions.tokenButtonDisabledOpacity ) )
  45. Private and con fi dential 72 Row( Modifier .defaultMinSizeConstraints(minHeight =

    height) .padding(contentPadding), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, )
  46. Private and con fi dential 73 if (icon != null)

    { Icon( modifier = Modifier.size(Dimensions.tokenButtonIconSize), imageVector = icon, tint = contentColor ) Spacer(modifier = Modifier.width(Dimensions.tokenButtonIconTextSpacing)) } Text( text = text, style = TextStyles.Action, color = contentColor, maxLines = 1, overflow = TextOverflow.Ellipsis )
  47. Private and con fi dential 74 if (icon != null)

    { Icon( modifier = Modifier.size(Dimensions.tokenButtonIconSize), imageVector = icon, tint = contentColor ) Spacer(modifier = Modifier.width(Dimensions.tokenButtonIconTextSpacing)) } Text( text = text, style = TextStyles.Action, color = contentColor, maxLines = 1, overflow = TextOverflow.Ellipsis )
  48. Private and con fi dential 75 if (icon != null)

    { Icon( modifier = Modifier.size(Dimensions.tokenButtonIconSize), imageVector = icon, tint = contentColor ) Spacer(modifier = Modifier.width(Dimensions.tokenButtonIconTextSpacing)) } Text( text = text, style = TextStyles.Action, color = contentColor, maxLines = 1, overflow = TextOverflow.Ellipsis )
  49. Private and con fi dential 76 @Composable fun Button( onClick:

    () -> Unit, text: String, modifier: Modifier = Modifier, color: Color = Colors.primary, contentColor: Color = Color.White, icon: ImageVector? = null, enabled: Boolean = true ) { ButtonSkeleton( onClick = onClick, modifier = modifier, backgroundColor = color, contentColor = contentColor, enabled = enabled, icon = icon, text = text ) }
  50. Private and con fi dential 78 @Composable fun MatchPhotos( leftPhoto:

    ImageBitmap, rightPhoto: ImageBitmap, badge: ImageVector, modifier: Modifier = Modifier )
  51. Private and con fi dential 80 MatchPhotos Colum n Ro

    w Phot o - vertical linear layout Phot o Badg e
  52. Private and con fi dential 81 MatchPhotos Colum n Ro

    w Phot o - horizontal linear layout Phot o Badg e
  53. Private and con fi dential 82 MatchPhotos Colum n Ro

    w Phot o Phot o Badg e - content
  54. Private and con fi dential 83 Image( asset = rightPhoto,

    modifier = Modifier .graphicsLayer( rotationZ = rotationDegrees, translationX = rightTranslateX, translationY = rightTranslateY ) .zIndex(rightZIndex) .size(photoSize) .clip(photoShape) .border( width = borderWidth, Colors.matchPhotosBorder, photoShape ) )
  55. Private and con fi dential 84 Image( asset = rightPhoto,

    modifier = Modifier .graphicsLayer( rotationZ = rotationDegrees, translationX = rightTranslateX, translationY = rightTranslateY ) .zIndex(rightZIndex) .size(photoSize) .clip(photoShape) .border( width = borderWidth, Colors.matchPhotosBorder, photoShape ) )
  56. Private and con fi dential 85 Image( asset = rightPhoto,

    modifier = Modifier .graphicsLayer( rotationZ = rotationDegrees, translationX = rightTranslateX, translationY = rightTranslateY ) .zIndex(rightZIndex) .size(photoSize) .clip(photoShape) .border( width = borderWidth, Colors.matchPhotosBorder, photoShape ) )
  57. Private and con fi dential 86 Image( asset = rightPhoto,

    modifier = Modifier .graphicsLayer( rotationZ = rotationDegrees, translationX = rightTranslateX, translationY = rightTranslateY ) .zIndex(rightZIndex) .size(photoSize) .clip(photoShape) .border( width = borderWidth, Colors.matchPhotosBorder, photoShape ) )
  58. Private and con fi dential 87 Image( asset = rightPhoto,

    modifier = Modifier .graphicsLayer( rotationZ = rotationDegrees, translationX = rightTranslateX, translationY = rightTranslateY ) .zIndex(rightZIndex) .size(photoSize) .clip(photoShape) .border( width = borderWidth, Colors.matchPhotosBorder, photoShape ) )
  59. Private and con fi dential 88 Row { Image( asset

    = leftPhoto, ... ) Image( asset = rightPhoto, ... ) }
  60. Private and con fi dential 89 Column{ Row { Image()

    Image() } Image( imageVector = badge, modifier = Modifier .size(iconSize) .align(Alignment.CenterHorizontally) .shadow( elevation = badgeElevation, shape = CircleShape ) ) }
  61. Private and con fi dential 91 @Composable fun CtaBox( media:

    @Composable () - > Unit, header: @Composable () -> Unit, content: @Composable () - > Unit, buttons: @Composable() -> Unit, modifier: Modifier = Modifier )
  62. Private and con fi dential 92 Column( verticalArrangement = Arrangement.Center,

    horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.padding(Spacing.xlg) ) { media() Spacer(modifier = Modifier.size(Spacing.xlg)) header() Spacer(modifier = Modifier.size(Spacing.md)) content() Spacer(modifier = Modifier.size(Spacing.xlg)) buttons() }
  63. Private and con fi dential 93 Column( verticalArrangement = Arrangement.Center,

    horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.padding(Spacing.xlg) ) { media() Spacer(modifier = Modifier.size(Spacing.xlg)) header() Spacer(modifier = Modifier.size(Spacing.md)) content() Spacer(modifier = Modifier.size(Spacing.xlg)) buttons() }
  64. Private and con fi dential 94 Column( verticalArrangement = Arrangement.Center,

    horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.padding(Spacing.xlg) ) { media() Spacer(modifier = Modifier.size(Spacing.xlg)); header() Spacer(modifier = Modifier.size(Spacing.md)) content() Spacer(modifier = Modifier.size(Spacing.xlg)) buttons() }
  65. Private and con fi dential 95 Column( verticalArrangement = Arrangement.Center,

    horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.padding(Spacing.xlg) ) { media() Spacer(modifier = Modifier.size(Spacing.xlg)) ;; header() Spacer(modifier = Modifier.size(Spacing.md)) content() Spacer(modifier = Modifier.size(Spacing.xlg)) buttons() }
  66. Private and con fi dential 96 Column( verticalArrangement = Arrangement.Center,

    horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.padding(Spacing.xlg) ) { media() Spacer(modifier = Modifier.size(Spacing.xlg)); header() Spacer(modifier = Modifier.size(Spacing.md)) content() Spacer(modifier = Modifier.size(Spacing.xlg)) buttons() }
  67. Private and con fi dential 97 Column( verticalArrangement = Arrangement.Center,

    horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.padding(Spacing.xlg) ) { media() Spacer(modifier = Modifier.size(Spacing.xlg)) header() Spacer(modifier = Modifier.size(Spacing.md)) content() Spacer(modifier = Modifier.size(Spacing.xlg)) buttons() }
  68. Private and con fi dential 98 @Composable fun MatchScreen(config: MatchConfig)

    { CtaBox( media = { MatchPhotos( .. ) }, header = { Text(text="",style=TextStyles.H1) }, content = { Text(text="",style=TextStyles.P1) }, buttons = { Button( text = " ... ", color = Color.White, contentColor = Color.Black ) } ) }
  69. Private and con fi dential 1. Design syste m •

    Token s • Component s • Pattern s 2. Android View intero p 3. Testin g 4. Integration 100
  70. Private and con fi dential 102 val count = remember

    { mutableStateOf(0) } Column { Text(text = "Compose Text Count = ${count.value}") AndroidView( viewBlock = { context -> TextView(context).apply { setOnClickListener { count.value = count.value + 1 } } }, update = { view -> view.text = "AndroidViews World! Count = ${count.value}" } ) }
  71. Private and con fi dential 103 val count = remember

    { mutableStateOf(0) } Column { Text(text = "Compose Text Count = ${count.value}") AndroidView( viewBlock = { context -> TextView(context).apply { setOnClickListener { count.value = count.value + 1 } } }, update = { view -> view.text = "AndroidViews World! Count = ${count.value}" } ) }
  72. Private and con fi dential 104 val count = remember

    { mutableStateOf(0) } Column { Text(text = "Compose Text Count = ${count.value}") AndroidView( viewBlock = { context -> TextView(context).apply { setOnClickListener { count.value = count.value + 1 } } }, update = { view -> view.text = "AndroidViews World! Count = ${count.value}" } ) }
  73. Private and con fi dential 105 val count = remember

    { mutableStateOf(0) } Column { Text(text = “Compose Count = ${count.value}") AndroidView( viewBlock = { context -> TextView(context).apply { setOnClickListener { count.value = count.value + 1 } } }, update = { view -> view.text = " .. . Count = ${count.value}" } ) }
  74. Private and con fi dential override fun onCreate() { val

    composeView = findViewById<ComposeView>( .. ) composeView.setContent { Text(text = "ComposeText") } } 108
  75. Private and con fi dential override fun onCreate() { val

    composeView = findViewById<ComposeView>( .. ) composeView.setContent { Text(text = "ComposeText") } } 109
  76. Private and con fi dential 1. Design syste m •

    Token s • Component s • Pattern s 2. Android View intero p 3. Testin g 4. Integration 110
  77. Private and con fi dential 112 object TestTagMatchScreen { const

    val button = "MatchScreenButton" ... } Button( ... modifier = Modifier.testTag(TestTagMatchScreen.button) )
  78. Private and con fi dential 113 @get:Rule val composeTestRule =

    createComposeRule() fun launchScreen() { composeTestRule.setContent { BadooTheme { MatchScreen() } } }
  79. Private and con fi dential 114 @Test fun matchScreenTest() {

    launchScreen() composeTestRule .onNodeWithTag(TestTagMatchScreen.button) .assertIsDisplayed() .assertTextEquals("Send Message") .performClick() } }
  80. Private and con fi dential 115 @Test fun interopTest() {

    launchScreen() onView(ViewMatchers.withId(R.dimen.counterAndroidView)) .perform(click()) .check(matches(withText("Hello From AndroidViews World! Count = 1"))) composeTestRule .onNodeWithText(“Compose Text Count = 1").assertExists() }
  81. Private and con fi dential 116 @Test fun interopTest() {

    launchScreen() onView(ViewMatchers.withId(R.dimen.counterAndroidView)) .perform(click()) .check(matches(withText("Hello From AndroidViews World! Count = 1"))) composeTestRule .onNodeWithText(“Compose Text Count = 1").assertExists() }
  82. Private and con fi dential 117 @Test fun interopTest() {

    launchScreen() onView(ViewMatchers.withId(R.id.counterAndroidView)) .perform(click()) .check(matches(withText("Hello From AndroidViews World! Count = 1"))) composeTestRule .onNodeWithText(“Compose Text Count = 1").assertExists() }
  83. Private and con fi dential 1. Design syste m •

    Token s • Component s • Pattern s 2. Android View intero p 3. Testin g 4. Integration 119
  84. Private and con fi dential Business logic integration — MVVM

    122 ViewModel Compose LiveData Method calls
  85. Private and con fi dential val value: String by observable.subscribeAsState("initial")

    val value: String by liveData.observeAsState(“initial") val value: String? by observable.subscribeAsState(null) val value: String? by liveData.observeAsState(null) Text("Value is $value") 123
  86. Private and con fi dential Business logic integration — MVP

    124 Method Calls ViewWrapper Compose State UI events Presenter
  87. Private and con fi dential ✅ How to create a

    design syste m ✅ How to integrate with the current U I ✅ How to tes t ✅ How to integrate within the business logic Summary 125
  88. Private and con fi dential • Set of main UI-component

    s • Experiment s • Non critical feature s • Wait for stable releas e • Ship to prod 🚀🚀🚀 Next steps 126
  89. Private and con fi dential • Design system talk •

    Compose pathway • Сompose samples • Repo from the talk • Bumble Tech blog Resources 127