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

Building a component library in Compose for a l...

Building a component library in Compose for a large banking application

Talk about building a component library in Compose for DNB Bank held at Droidcon in San Francisco on June 8.

Odin Asbjornsen

June 20, 2023
Tweet

More Decks by Odin Asbjornsen

Other Decks in Programming

Transcript

  1. Building a component library in Compose for a large-scale banking

    application Odin Asbjørnsen Android Developer @ DNB
  2. What this talk is • Opinionated way that made Compose

    work for us • Not meant as a blueprint for other projects
  3. Agenda • Who is DNB Bank? • Starting the Jetpack

    Compose initiative • Eufemia design system • Design system architecture • Building a component library with Compose • Making sure we deliver components with banking level quality • Release & dependency management • Learnings and challenges
  4. Who is DNB Bank? • Bank in Norway, Europe •

    We help millions of people move billions of NOK every day • Both within private and business banking • Of fi ces around the world
  5. Who is DNB Bank? • Most used mobile bank in

    Norway • We provide • Mobile bank services • Fund trading services • Pension services • Loan services • ++
  6. Who is DNB Bank? • 3 mobile banking applications +

    web applications • MobileBank • Spare • Bedrift
  7. Starting the Jetpack Compose initiative • Started early last year

    • How can we make UI development faster? • How do we start using Compose in our applications?
  8. Starting the Jetpack Compose initiative • Change of mindset •

    Declarative programming style • Held workshops with teams adopting Compose • Heavy code reviews
  9. Starting the Jetpack Compose initiative • Monolithic repo with different

    architecture • One app had single activity architecture • One app had multiple activity architecture • One app is not part of mono-repo • They all used one design system
  10. Eufemia Design System • Open-source design system • Shared with

    web, iOS and Android • Setup to follow industry regulatory standards for A11y • Maintained by a group of developers and designers
  11. How to implement the design system? • You can use

    Material out of the box • Can make your own based on Compose foundation • We needed to compliant with Eufemia • Tried to wrap Material
  12. How to implement the design system? • Wrapping Material •

    Made it easy to start • Components where well tested • It was quite close to Eufemia
  13. Eufemia design in Android • Internal platform speci fi c

    version of Eufemia • View and Compose support • Not 1 to 1 with Material • Colours and Typography is different
  14. How to implement the design system? Theming • Supporting 30

    different colour palettes for our apps
  15. How to implement the design system? Theming • Contains all

    our colours • Have our own colour class • Provided through a colour palette
  16. How to implement the design system? Theming @Stable public class

    EufemiaColors( interaction: Color, interactionLight: Color, interactionDark: Color, background: Color, surface: Color, surfaceBackgroundPrimary: Color, surfaceBackgroundSecondary: Color, highlightSurface: Color, highlightOnSurface: Color, textPrimary: Color, textSecondary: Color, accessory: Color, *** ** * )
  17. How to implement the design system? Theming public val LightEufemiaColorPalette:

    EufemiaColors = EufemiaColors( interaction = SeaGreen, background = LightGrey, surface = White, surfaceBackgroundPrimary = OceanGreen, surfaceBackgroundSecondary = EmeraldGreen, highlightSurface = OutlineGrey, highlightOnSurface = CoalAlpha08, *** ** * )
  18. How to implement the design system? Theming public val LightEufemiaColorPalette:

    EufemiaColors public val LightSagaEufemiaColorPalette: EufemiaColors public val DarkEufemiaColorPalette: EufemiaColors *** ** *
  19. How to implement the design system? • Typography • Also

    provided through the theme • Providing a custom class that holds the compose text styles Theming
  20. How to implement the design system? Theming public data class

    EufemiaTypography( val title: TextStyle, val titleDemi: TextStyle, val titleEmphasized: TextStyle, val titleMedium: TextStyle, val titleMediumEmphasized: TextStyle, val titleLarge: TextStyle, *** ** * )
  21. How to implement the design system? • Shapes • Also

    provided through the theme Theming
  22. How to implement the design system? Theming @Immutable public class

    EufemiaShape( public val small: CornerBasedShape = RoundedCornerShape(4.dp), public val medium: CornerBasedShape = RoundedCornerShape(4.dp), public val large: CornerBasedShape = RoundedCornerShape(8.dp), )
  23. How to implement the design system? Theming • How are

    these properties provided through the theme?
  24. How to implement the design system? Theming @Composable public fun

    EufemiaTheme( overlayType: OverlayType, isNightMode: Boolean = isSystemInDarkTheme(), loyaltyProgram: LoyaltyProgram = ThemeServiceWrapper.get(), shape: EufemiaShape = EufemiaShape(), content: @Composable () -> Unit )
  25. How to implement the design system? Theming @Composable public fun

    EufemiaTheme( overlayType: OverlayType, isNightMode: Boolean = isSystemInDarkTheme(), loyaltyProgram: LoyaltyProgram = ThemeServiceWrapper.get(), shape: EufemiaShape = EufemiaShape(), content: @Composable () -> Unit ) { * * * ProvideEufemiaColors( isNightMode = isNightMode, loyaltyProgram = loyaltyProgram, ) { CompositionLocalProvider( LocalEufemiaTypography provides provideTypography() ) { MaterialTheme( colors = getMaterialColors(isNightMode = isNightMode), content = content ) } } }
  26. How to implement the design system? Theming public object EufemiaTheme

    { public val colors: EufemiaColors @Composable @ReadOnlyComposable get() = LocalEufemiaColors.current public val typography: EufemiaTypography @Composable @ReadOnlyComposable get() = LocalEufemiaTypography.current *** ** * }
  27. How to implement the design system? Theming Text( text =

    title, style = EufemiaTheme.typography.bodyEmphasized, color = EufemiaTheme.colors.textPrimary, )
  28. How to implement the design system? Atoms • Atoms are

    the fundamental building blocks • Buttons • Chips • Checkbox • ++
  29. How to implement the design system? Atoms • Atoms have

    their own fi gma instance • Designers need approval to add atoms • These are speci fi c for platform
  30. Eufemia design in Android • Providing a design system for

    3 different apps + other libraries • A lot of similar components • Keeping it consistent through the DNB domain
  31. Building a component library in Compose @Composable public fun CardPromo(

    title: String, description: String, illustrationDrawable: DrawableData, backgroundColor: Color, primaryButtonText: String?, primaryButtonOnClicked: (() -> Unit)?, modifier: Modifier = Modifier, )
  32. Building a component library in Compose @Composable public fun CardPromo(

    illustration: @Composable () -> Unit, button: @Composable () -> Unit, textContent: @Composable () -> Unit, modifier: Modifier = Modifier, )
  33. Building a component library in Compose • Slot APIs •

    Decreases the API surface • Reduces coupling between components • Can become to fl exible @Composable public fun CardPromo( illustration: @Composable () -> Unit, button: @Composable () -> Unit, textContent: @Composable () -> Unit, modifier: Modifier = Modifier, )
  34. Building a component library in Compose • Where to draw

    the line? • Domain speci fi c vs. Generic component? • Should we only depend on the library and not Eufemia-atoms?
  35. Building a component library in Compose • There is no

    solution that fi ts all • Our mission was to help the developers • Did not want developers to reinvent the wheel
  36. Building a component library in Compose • Applications who only

    depend on component library forced domain speci fi c components into the library • Did not want to expose other apps domain to all the apps
  37. Building a component library in Compose Eufemia Component library App1

    App2 uses uses uses uses uses Module Module Module Module Module Module
  38. Building a component library in Compose • Figma fi le

    for Eufemia and Component library • New components needs review from designers/community
  39. Making sure we deliver components with banking- level quality •

    Compose + Material helps us out of the box • Enforces touch sizes by default • Content description is required from API side in Compose
  40. Making sure we deliver components with banking- level quality •

    Permutation based testing • n X Font • n X Display • dark vs light mode • loyalty programs
  41. Making sure we deliver components with banking- level quality •

    Use Showkase from Airbnb • Annotate our component with @Preview or @ShowkaseComposable • Will generate an activity to browse your components • Helps us see which components we have
  42. Making sure we deliver components with banking- level quality @Composable

    public fun CardPromo( title: String, description: String, illustrationDrawable: DrawableData, modifier: Modifier = Modifier, backgroundColor: Color = EufemiaTheme.colors.surface, closeOnClicked: (() -> Unit)? = null, primaryButtonText: String? = null, primaryButtonOnClicked: () -> Unit = {}, textButtonText: String? = null, textButtonOnClicked: (() -> Unit) = {}, textButtonIsLink: Boolean = true, )
  43. Making sure we deliver components with banking- level quality @ShowkaseComposable

    @Composable public fun CardPromoPreview( @PreviewParameter(LoyaltyProgramParameterProvider::class) params: LoyaltyProgram, ) { EufemiaTheme(loyaltyProgram = params, overlayType = OverlayType.STANDARD) { CardPromo( modifier = Modifier.fillMaxWidth(), title = "DNBs mortgage", description = "A mortgage loan can be a good solution if you want to buy a house. You can calculate how much you can get here!", illustrationDrawable = DrawableData( drawableRes = R.drawable.ic_illustration_example_1, contentDescription = “Mortgage illustration” ), closeOnClicked = {}, primaryButtonText = "Calculate mortgage loan", primaryButtonOnClicked = {}, textButtonText = "Read more about mortgages at dnb.no", textButtonOnClicked = {} ) } }
  44. Making sure we deliver components with banking- level quality @ShowkaseComposable

    @Composable public fun CardPromoPreview( @PreviewParameter(LoyaltyProgramParameterProvider::class) params: LoyaltyProgram, ) { EufemiaTheme(loyaltyProgram = params, overlayType = OverlayType.STANDARD) { CardPromo( modifier = Modifier.fillMaxWidth(), title = "DNBs mortgage", description = "A mortgage loan can be a good solution if you want to buy a house. You can calculate how much you can get here!", illustrationDrawable = DrawableData( drawableRes = R.drawable.ic_illustration_example_1, contentDescription = “Mortgage illustration” ), closeOnClicked = {}, primaryButtonText = "Calculate mortgage loan", primaryButtonOnClicked = {}, textButtonText = "Read more about mortgages at dnb.no", textButtonOnClicked = {} ) } } @PreviewParameter(LoyaltyProgramParameterProvider::class) params: LoyaltyProgram,
  45. Making sure we deliver components with banking- level quality •

    Paparazzi for screenshot testing con fi guration permutations • Require all components to be covered • Can be integrated with Showkase
  46. Making sure we deliver components with banking- level quality •

    POCs on paparazzi + a11y • Paparazzi version 1.3.0+ • Screenshot testing semantic nodes • AccessibilityRenderExtension
  47. Making sure we deliver components with banking- level quality class

    CardPromoScA11yTest { @get: Rule val paparazzi = Paparazzi( deviceConfig = DeviceConfig.PIXEL_C, renderExtensions = setOf(AccessibilityRenderExtension()) ) @Test fun cardPromoLightModeStandardNormalFont() { paparazzi.snapshot { CardPromoPreview(params = LoyaltyProgram.DEFAULT) } } }
  48. Making sure we deliver components with banking- level quality class

    CardPromoScA11yTest { @get: Rule val paparazzi = Paparazzi( deviceConfig = DeviceConfig.PIXEL_C, renderExtensions = setOf(AccessibilityRenderExtension()) ) @Test fun cardPromoLightModeStandardNormalFont() { paparazzi.snapshot { CardPromoPreview(params = LoyaltyProgram.DEFAULT) } } } renderExtensions = setOf(AccessibilityRenderExtension())
  49. Making sure we deliver components with banking- level quality •

    Locally hosted preview screenshot testing from AGP 8.2+ • Announced at Google IO 2023 • Integrate well with @Preview • Great option to Showkase + Paparazzi
  50. Making sure we deliver components with banking- level quality •

    We use Robolectric heavily for UI tests • Runs without spinning up an emulator
  51. Making sure we deliver components with banking- level quality •

    Preview helps us check device con fi gurations during development
  52. Making sure we deliver components with banking- level quality @Preview

    @Composable public fun CardPromoPreview( ) @Preview
  53. Making sure we deliver components with banking- level quality annotation

    class Preview( val name: String = “", val group: String = “", @IntRange(from = 1) val apiLevel: Int = -1, val widthDp: Int = -1, val heightDp: Int = -1, val locale: String = “", @FloatRange(from = 0.01) val fontScale: Float = 1f, val showSystemUi: Boolean = false, val showBackground: Boolean = false, val backgroundColor: Long = 0, @UiMode val uiMode: Int = 0, @Device val device: String = Devices.DEFAULT, @Wallpaper val wallpaper: Int = Wallpapers.NONE, )
  54. Making sure we deliver components with banking- level quality @Preview(

    name = "CardPromo dark high font", uiMode = Configuration.UI_MODE_NIGHT_YES, fontScale = 2f, ) @Composable public fun CardPromoPreview( )
  55. Making sure we deliver components with banking- level quality @Preview(

    name = "CardPromo dark high font", uiMode = Configuration.UI_MODE_NIGHT_YES, fontScale = 2f, ) @Preview( name = "CardPromo light high font", uiMode = Configuration.UI_MODE_NIGHT_NO, fontScale = 2f, ) @Composable public fun CardPromoPreview( )
  56. Making sure we deliver components with banking- level quality @Preview(

    name = "CardPromo dark high font", uiMode = Configuration.UI_MODE_NIGHT_YES, fontScale = 2f, ) @Preview( name = "CardPromo light high font", uiMode = Configuration.UI_MODE_NIGHT_NO, fontScale = 2f, ) annotation class HighFontPreviews
  57. Making sure we deliver components with banking- level quality •

    Ensuring Code Quality • A lot of easy mistakes to make in Compose • Static code checks helps us avoid these
  58. Making sure we deliver components with banking- level quality @Composable

    fun InputTextField() { var text = mutableStateOf("") TextField(value = text, onValueChange = { text.value = it }) }
  59. Making sure we deliver components with banking- level quality @Composable

    fun InputTextField() { var text = mutableStateOf("") TextField(value = text, onValueChange = { text.value = it }) } var text = mutableStateOf("") • Rule violation
  60. Making sure we deliver components with banking- level quality @Composable

    fun InputTextField() { var text = remember { mutableStateOf("") } TextField(value = text, onValueChange = { text.value = it }) } var text = remember { mutableStateOf("") }
  61. Making sure we deliver components with banking- level quality @Composable

    fun InputTextField() { var text = remember { mutableStateOf("") } TextField(value = text, onValueChange = { text.value = it }) } var text = remember { mutableStateOf("") } • State should be remembered to avoid creating new state instance on each recomposition
  62. Release & dependency management • Nexus • Dev release after

    merge • Testing during development • Local substitution • Publish maven local • Showkase • Previews
  63. Learnings and challenges • Using custom theme in previews without

    setting loyalty program type crashes the preview
  64. Learnings and challenges • This will crash :( @Preview @Composable

    fun ComponentPreview() { EufemiaTheme() { SomeComponent() } }
  65. Learnings and challenges @Composable public fun EufemiaTheme( overlayType: OverlayType, isNightMode:

    Boolean = isSystemInDarkTheme(), loyaltyProgram: LoyaltyProgram = ThemeServiceWrapper.get(), shape: EufemiaShape = EufemiaShape(), content: @Composable () -> Unit )
  66. Learnings and challenges @Composable public fun EufemiaTheme( overlayType: OverlayType, isNightMode:

    Boolean = isSystemInDarkTheme(), loyaltyProgram: LoyaltyProgram = ThemeServiceWrapper.get(), shape: EufemiaShape = EufemiaShape(), content: @Composable () -> Unit ) loyaltyProgram: LoyaltyProgram = ThemeServiceWrapper.get(),
  67. Learnings and challenges @Composable private fun loyaltyProgram(): LoyaltyProgram { if

    (LocalInspectionMode.current) return LoyaltyProgram.DEFAULT return ThemeServiceWrapper.get().getCurrentProgram().toLoyaltyProgram() }
  68. Learnings and challenges • When starting to build a component

    library • Have close conversation with designers • Make sure you are on the same page
  69. Learnings and challenges • Start strict and open up •

    Hard to close up if users depend on it being loose
  70. QR Codes Scalable UI testing solutions 
 Google I/O Showkase

    - Airbnb Twitter Compose rules Paparazzi