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

Composable Preview Driven Development: TDD-fyin...

Composable Preview Driven Development: TDD-fying your UI with ease!

Tech-talk at Droidcon Lisbon 2024
-----------------------------------------------------------------------------------
So you're already familiar with Compose UI… but have you considered using Test-Driven Development (TDD) with Composable Previews to refactor your UI quickly and with confidence?

Allow me to introduce you to “Composable Preview Driven Development”!

In this tech talk, you will learn…
- How writing Previews first can improve your code quality
- How to effortlessly turn Previews into automated tests to reliably refactor UI
- Composable Preview tips and tricks that are not widely known

Make sure not to miss this opportunity to explore the powerful combination of Test-Driven Development and Composable Previews!

No previous TDD knowledge is required!

Sergio Sastre Flórez

September 08, 2024
Tweet

More Decks by Sergio Sastre Flórez

Other Decks in Technology

Transcript

  1. @Gio_Sastre TDD basics TDD R E D T E S

    T G R E E
 N T E S T R E F A
 C T O R I N G Write a failing test 1 Write minimal code to make the test pass 2 Refactor code Verify code changes by running tests 3
  2. @Gio_Sastre CPDD basics Write @Previews 1 Refactor code Verify UI

    screenshots 3 Write code to make the UI look as expected 2 Record UI screenshot CPDD P R E I E W V R E C O
 R D R E F A
 C T O R I N G
  3. @Gio_Sastre TDD & CPDD CPDD P R E I E

    W V R E C O
 R D R E F A
 C T O R I N G TDD R E D T E S T G R E E
 N T E S T R E F A
 C T O R I N G
  4. @Gio_Sastre @Previews last Content fun SubscriptionScreen ( @Composable Header (

    … ) } subscriptions : ImmutableList<SubscriptionItem> , ) {
  5. @Gio_Sastre @Previews last Content fun SubscriptionScreen ( @Composable Header (

    … ) } subscriptions : ImmutableList<SubscriptionItem> , SubscriptionsLazyColumn ( items = subscriptions ) { … } ) {
  6. @Gio_Sastre @Previews last Content fun SubscriptionScreen ( @Composable Header (

    … ) } subscriptions : ImmutableList<SubscriptionItem> , SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( enabled = ) ) { true
  7. @Gio_Sastre @Previews last Content fun SubscriptionScreen ( @Composable Header (

    … ) } subscriptions : ImmutableList<SubscriptionItem> , SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( enabled = ) ) { true
  8. enabled = @Gio_Sastre @Previews last Loading Header ( … )

    } subscriptions : ImmutableList<SubscriptionItem> , ) { SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( isLoading : Boolean , ) fun SubscriptionScreen ( @Composable true
  9. enabled = @Gio_Sastre @Previews last Loading Header ( … )

    } subscriptions : ImmutableList<SubscriptionItem> , ) { SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( isLoading : Boolean , ) fun SubscriptionScreen ( @Composable true
  10. enabled = @Gio_Sastre @Previews last Loading Header ( … )

    } subscriptions : ImmutableList<SubscriptionItem> , ) { SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( isLoading : Boolean , } else } if ( isLoading ) { ShimmerRows ( … ) ) { fun SubscriptionScreen ( @Composable true
  11. enabled = @Gio_Sastre @Previews last Loading Header ( … )

    } subscriptions : ImmutableList<SubscriptionItem> , ) { SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( isLoading : Boolean , } else } if ( isLoading ) { ShimmerRows ( … ) ) { fun SubscriptionScreen ( @Composable true
  12. ! isLoading enabled = @Gio_Sastre @Previews last Loading Header (

    … ) } subscriptions : ImmutableList<SubscriptionItem> , ) { SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( isLoading : Boolean , } else } if ( isLoading ) { ShimmerRows ( … ) ) { fun SubscriptionScreen ( @Composable
  13. ! isLoading enabled = @Gio_Sastre @Previews last Loading Header (

    … ) } subscriptions : ImmutableList<SubscriptionItem> , ) { SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( isLoading : Boolean , } else } if ( isLoading ) { ShimmerRows ( … ) ) { fun SubscriptionScreen ( @Composable
  14. { ! isLoading @Gio_Sastre @Previews last fun SubscriptionScreen ( @Composable

    Header ( … ) } subscriptions : ImmutableList<SubscriptionItem> , ) { isLoading : Boolean , SubscriptionsLazyColumn ( items = subscriptions ) { … } if ( isLoading ) { } else } ShimmerRows ( … ) BuySubscriptionButton ( enabled = ) isError : Boolean , Error
  15. { ! isLoading @Gio_Sastre @Previews last fun SubscriptionScreen ( @Composable

    Header ( … ) } subscriptions : ImmutableList<SubscriptionItem> , ) { isLoading : Boolean , SubscriptionsLazyColumn ( items = subscriptions ) { … } if ( isLoading ) { } else } ShimmerRows ( … ) BuySubscriptionButton ( enabled = ) isError : Boolean , Error
  16. { if ! isLoading ( ! isError ) @Gio_Sastre @Previews

    last fun SubscriptionScreen ( @Composable Header ( … ) } subscriptions : ImmutableList<SubscriptionItem> , ) { isLoading : Boolean , SubscriptionsLazyColumn ( items = subscriptions ) { … } if ( isLoading ) { } else } ShimmerRows ( … ) BuySubscriptionButton ( enabled = ) isError : Boolean , Error
  17. { if ! isLoading ( ! isError ) @Gio_Sastre @Previews

    last fun SubscriptionScreen ( @Composable Header ( … ) } subscriptions : ImmutableList<SubscriptionItem> , ) { isLoading : Boolean , SubscriptionsLazyColumn ( items = subscriptions ) { … } if ( isLoading ) { } else } ShimmerRows ( … ) BuySubscriptionButton ( enabled = ) isError : Boolean , Error
  18. { if ! isLoading ( ! isError ) @Gio_Sastre @Previews

    last fun SubscriptionScreen ( @Composable Header ( … ) } subscriptions : ImmutableList<SubscriptionItem> , ) { isLoading : Boolean , SubscriptionsLazyColumn ( items = subscriptions ) { … } if ( isLoading ) { } else } ShimmerRows ( … ) BuySubscriptionButton ( enabled = ) isError : Boolean , ErrorButton ( ) if ( isError ) { } else { } Error
  19. { if ! isLoading ( ! isError ) @Gio_Sastre @Previews

    last fun SubscriptionScreen ( @Composable Header ( … ) } subscriptions : ImmutableList<SubscriptionItem> , ) { isLoading : Boolean , SubscriptionsLazyColumn ( items = subscriptions ) { … } if ( isLoading ) { } else } ShimmerRows ( … ) BuySubscriptionButton ( enabled = ) isError : Boolean , ErrorButton ( ) if ( isError ) { } else { } Error
  20. @Gio_Sastre - Too much logic in UI code Issues with

    “@Previews last” development 1 @Composables code is harder to understand 2 @Preview cannot render due to “smart @Composables” - No clear separation of UI states - Big or deeply nested objects - ViewModels i.e. those having as arguments
  21. @Gio_Sastre One Preview per Screen UI State One Composable per

    Screen UI State Don’t avoid code duplication CPDD Preview Prerecord Multi-Previews over a single Preview
  22. @Gio_Sastre Content ) { fun ContentSubscriptionScreenPreview ( @Composable } @Preview

    AppTheme { } fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , ) { } CPDD Preview Prerecord
  23. @Gio_Sastre Content ) { fun ContentSubscriptionScreenPreview ( @Composable } @Preview

    ContentSubscriptionScreen ( subscriptions ) val subscriptions = … AppTheme { } fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , ) { } CPDD Preview Prerecord
  24. @Gio_Sastre Content ) { fun ContentSubscriptionScreenPreview ( @Composable } @Preview

    ContentSubscriptionScreen ( subscriptions ) val subscriptions = … AppTheme { } Header ( … ) fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , ) { } CPDD Preview Prerecord
  25. @Gio_Sastre Content ) { fun ContentSubscriptionScreenPreview ( @Composable } @Preview

    ContentSubscriptionScreen ( subscriptions ) val subscriptions = … AppTheme { } Header ( … ) SubscriptionsLazyColumn ( items = subscriptions ) { … } fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , ) { } CPDD Preview Prerecord
  26. @Gio_Sastre Content ) { fun ContentSubscriptionScreenPreview ( @Composable } @Preview

    ContentSubscriptionScreen ( subscriptions ) val subscriptions = … AppTheme { } Header ( … ) SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( enabled = true ) fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , ) { } CPDD Preview Prerecord
  27. @Gio_Sastre ) { @Preview fun LoadingSubscriptionScreenPreview ( @Composable } AppTheme

    { } fun LoadingSubscriptionScreen ( @Composable ) { } Loading CPDD Preview Prerecord
  28. @Gio_Sastre LoadingSubscriptionScreen ( ) ) { @Preview fun LoadingSubscriptionScreenPreview (

    @Composable } AppTheme { } fun LoadingSubscriptionScreen ( @Composable ) { } Loading CPDD Preview Prerecord
  29. @Gio_Sastre LoadingSubscriptionScreen ( ) ) { @Preview fun LoadingSubscriptionScreenPreview (

    @Composable } AppTheme { } Header ( … ) fun LoadingSubscriptionScreen ( @Composable ) { } Loading CPDD Preview Prerecord
  30. @Gio_Sastre LoadingSubscriptionScreen ( ) ) { @Preview fun LoadingSubscriptionScreenPreview (

    @Composable } AppTheme { } Header ( … ) fun LoadingSubscriptionScreen ( @Composable ) { } Loading ShimmerRows ( … ) CPDD Preview Prerecord
  31. @Gio_Sastre LoadingSubscriptionScreen ( ) ) { @Preview fun LoadingSubscriptionScreenPreview (

    @Composable } AppTheme { } Header ( … ) BuySubscriptionButton ( enabled = false ) fun LoadingSubscriptionScreen ( @Composable ) { } Loading ShimmerRows ( … ) CPDD Preview Prerecord
  32. @Gio_Sastre ) { fun ErrorSubscriptionScreenPreview ( @Composable } @Preview AppTheme

    { } fun ErrorSubscriptionScreen ( @Composable ) { } CPDD Preview Prerecord Error
  33. @Gio_Sastre ErrorSubscriptionScreen ( ) ) { fun ErrorSubscriptionScreenPreview ( @Composable

    } @Preview AppTheme { } fun ErrorSubscriptionScreen ( @Composable ) { } CPDD Preview Prerecord Error
  34. @Gio_Sastre ErrorSubscriptionScreen ( ) ) { fun ErrorSubscriptionScreenPreview ( @Composable

    } @Preview AppTheme { } Header ( … ) fun ErrorSubscriptionScreen ( @Composable ) { } CPDD Preview Prerecord Error
  35. @Gio_Sastre ErrorSubscriptionScreen ( ) ) { fun ErrorSubscriptionScreenPreview ( @Composable

    } @Preview AppTheme { } Header ( … ) ErrorButton () fun ErrorSubscriptionScreen ( @Composable ) { } CPDD Preview Prerecord Error
  36. @Gio_Sastre fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , )

    { Header ( … ) SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( } fun ErrorSubscriptionScreen ( @Composable Header ( … ) ErrorButton () } fun LoadingSubscriptionScreen ( @Composable Header ( … ) BuySubscriptionButton ( } ShimmerRows ( … ) enabled = false )
  37. data class Content ( ) sealed class ScreenUiState ( subscriptions

    : ImmutableList<SubscriptionItem> , ) { : ScreenUiState : ScreenUiState data object Loading ( ) : ScreenUiState data object Error ( ) } @Gio_Sastre fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , ) { Header ( … ) SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( } fun ErrorSubscriptionScreen ( @Composable Header ( … ) ErrorButton () } fun LoadingSubscriptionScreen ( @Composable Header ( … ) BuySubscriptionButton ( } ShimmerRows ( … ) enabled = false )
  38. data class Content ( ) sealed class ScreenUiState ( subscriptions

    : ImmutableList<SubscriptionItem> , ) { : ScreenUiState : ScreenUiState data object Loading ( ) : ScreenUiState data object Error ( ) } @Gio_Sastre fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , ) { Header ( … ) SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( } fun ErrorSubscriptionScreen ( @Composable Header ( … ) ErrorButton () } fun LoadingSubscriptionScreen ( @Composable Header ( … ) BuySubscriptionButton ( } ShimmerRows ( … ) enabled = false ) fun SubscriptionScreen () @Composable } val screenUiState = viewModel.screenUiState.collectAsState() when ( screenUiState ) { is Content -> ContentSubscriptionScreen ( screenUiState.subscriptions ) Loading -> LoadingSubscriptionScreen ( ) Error -> ErrorSubscriptionScreen ( ) }
  39. data class Content ( ) sealed class ScreenUiState ( subscriptions

    : ImmutableList<SubscriptionItem> , ) { : ScreenUiState : ScreenUiState data object Loading ( ) : ScreenUiState data object Error ( ) } @Gio_Sastre fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , ) { Header ( … ) SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( } fun ErrorSubscriptionScreen ( @Composable Header ( … ) ErrorButton () } fun LoadingSubscriptionScreen ( @Composable Header ( … ) BuySubscriptionButton ( } ShimmerRows ( … ) enabled = false ) fun SubscriptionScreen () @Composable } val screenUiState = viewModel.screenUiState.collectAsState() when ( screenUiState ) { is Content -> ContentSubscriptionScreen ( screenUiState.subscriptions ) Loading -> LoadingSubscriptionScreen ( ) Error -> ErrorSubscriptionScreen ( ) }
  40. data class Content ( ) sealed class ScreenUiState ( subscriptions

    : ImmutableList<SubscriptionItem> , ) { : ScreenUiState : ScreenUiState data object Loading ( ) : ScreenUiState data object Error ( ) } @Gio_Sastre fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , ) { Header ( … ) SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( } fun ErrorSubscriptionScreen ( @Composable Header ( … ) ErrorButton () } fun LoadingSubscriptionScreen ( @Composable Header ( … ) BuySubscriptionButton ( } ShimmerRows ( … ) enabled = false ) fun SubscriptionScreen () @Composable } val screenUiState = viewModel.screenUiState.collectAsState() when ( screenUiState ) { is Content -> ContentSubscriptionScreen ( screenUiState.subscriptions ) Loading -> LoadingSubscriptionScreen ( ) Error -> ErrorSubscriptionScreen ( ) }
  41. data class Content ( ) sealed class ScreenUiState ( subscriptions

    : ImmutableList<SubscriptionItem> , ) { : ScreenUiState : ScreenUiState data object Loading ( ) : ScreenUiState data object Error ( ) } @Gio_Sastre fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , ) { Header ( … ) SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( } fun ErrorSubscriptionScreen ( @Composable Header ( … ) ErrorButton () } fun LoadingSubscriptionScreen ( @Composable Header ( … ) BuySubscriptionButton ( } ShimmerRows ( … ) enabled = false ) fun SubscriptionScreen () @Composable } val screenUiState = viewModel.screenUiState.collectAsState() when ( screenUiState ) { is Content -> ContentSubscriptionScreen ( screenUiState.subscriptions ) Loading -> LoadingSubscriptionScreen ( ) Error -> ErrorSubscriptionScreen ( ) }
  42. @Gio_Sastre ) { fun ContentSubscriptionScreenPreview ( @Composable } @Preview ContentSubscriptionScreen

    ( subscriptions ) val subscriptions = … AppTheme { } CPDD Preview Prerecord
  43. @Gio_Sastre ) { fun ContentSubscriptionScreenPreview ( @Composable } @Preview ContentSubscriptionScreen

    ( subscriptions ) val subscriptions = … AppTheme { } @Preview ( fontScale = 1.3f ) CPDD Preview Prerecord
  44. @Gio_Sastre ) { fun ContentSubscriptionScreenPreview ( @Composable } @Preview ContentSubscriptionScreen

    ( subscriptions ) val subscriptions = … AppTheme { } @Preview ( fontScale = 1.3f ) S C R O L L A B L E CPDD Preview Prerecord
  45. @Gio_Sastre Auto-generate tests from @Previews @Previews source location Fully automated

    screenshotTest only + main only main only Fully automated Roborazzi Paparazzi Compose Preview
  46. @Gio_Sastre fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , )

    { Header ( … ) SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( } fun ErrorSubscriptionScreen ( @Composable ErrorButton () } fun LoadingSubscriptionScreen ( @Composable BuySubscriptionButton ( } ShimmerRows ( … ) enabled = false) enabled = true ) TopBar ( … ) Terms( … ) Header ( … ) Terms( … ) TopBar ( … ) Header ( … ) TopBar ( … ) Terms( … )
  47. @Gio_Sastre fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , )

    { Header ( … ) SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( } fun ErrorSubscriptionScreen ( @Composable ErrorButton () } fun LoadingSubscriptionScreen ( @Composable BuySubscriptionButton ( } ShimmerRows ( … ) enabled = false) enabled = true ) TopBar ( … ) Terms( … ) Header ( … ) Terms( … ) TopBar ( … ) Header ( … ) TopBar ( … ) Terms( … )
  48. @Gio_Sastre fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , )

    { Header ( … ) SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( } fun ErrorSubscriptionScreen ( @Composable ErrorButton () } fun LoadingSubscriptionScreen ( @Composable BuySubscriptionButton ( } ShimmerRows ( … ) enabled = false) enabled = true ) TopBar ( … ) Terms( … ) Header ( … ) Terms( … ) TopBar ( … ) Header ( … ) TopBar ( … ) Terms( … ) ) { fun ScaffoldSubscriptionScreen ( @Composable } Header ( … ) TopBar ( … ) Terms( … )
  49. @Gio_Sastre fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , )

    { Header ( … ) SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( } fun ErrorSubscriptionScreen ( @Composable ErrorButton () } fun LoadingSubscriptionScreen ( @Composable BuySubscriptionButton ( } ShimmerRows ( … ) enabled = false) enabled = true ) TopBar ( … ) Terms( … ) Header ( … ) Terms( … ) TopBar ( … ) Header ( … ) TopBar ( … ) Terms( … ) ) { fun ScaffoldSubscriptionScreen ( @Composable } Header ( … ) TopBar ( … ) Terms( … ) @Composable subscriptionsPlaceholder : ( ) -> Unit, subscriptionsPlaceholder( )
  50. @Gio_Sastre fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , )

    { SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( } fun ErrorSubscriptionScreen ( @Composable ErrorButton () } fun LoadingSubscriptionScreen ( @Composable BuySubscriptionButton ( } ShimmerRows ( … ) enabled = false) enabled = true ) Header ( … ) Terms( … ) TopBar ( … ) Header ( … ) TopBar ( … ) Terms( … ) ) { fun ScaffoldSubscriptionScreen ( @Composable } Header ( … ) TopBar ( … ) Terms( … ) @Composable subscriptionsPlaceholder : ( ) -> Unit, subscriptionsPlaceholder( ) ScaffoldSubscriptionScreen { }
  51. @Gio_Sastre fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , )

    { SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( } fun ErrorSubscriptionScreen ( @Composable ErrorButton () } fun LoadingSubscriptionScreen ( @Composable BuySubscriptionButton ( } ShimmerRows ( … ) enabled = false) enabled = true ) Header ( … ) Terms( … ) TopBar ( … ) Header ( … ) TopBar ( … ) Terms( … ) ) { fun ScaffoldSubscriptionScreen ( @Composable } Header ( … ) TopBar ( … ) Terms( … ) @Composable subscriptionsPlaceholder : ( ) -> Unit, subscriptionsPlaceholder( ) ScaffoldSubscriptionScreen { } verify
  52. @Gio_Sastre fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , )

    { SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( } fun ErrorSubscriptionScreen ( @Composable ErrorButton () } fun LoadingSubscriptionScreen ( @Composable BuySubscriptionButton ( } ShimmerRows ( … ) enabled = false) enabled = true ) Header ( … ) Terms( … ) TopBar ( … ) Header ( … ) TopBar ( … ) Terms( … ) ) { fun ScaffoldSubscriptionScreen ( @Composable } Header ( … ) TopBar ( … ) Terms( … ) @Composable subscriptionsPlaceholder : ( ) -> Unit, subscriptionsPlaceholder( ) ScaffoldSubscriptionScreen { } verify
  53. @Gio_Sastre fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , )

    { SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( } fun ErrorSubscriptionScreen ( @Composable ErrorButton () } fun LoadingSubscriptionScreen ( @Composable BuySubscriptionButton ( } ShimmerRows ( … ) enabled = false) enabled = true ) Header ( … ) Terms( … ) TopBar ( … ) Header ( … ) TopBar ( … ) Terms( … ) ) { fun ScaffoldSubscriptionScreen ( @Composable } Header ( … ) TopBar ( … ) Terms( … ) @Composable subscriptionsPlaceholder : ( ) -> Unit, subscriptionsPlaceholder( ) ScaffoldSubscriptionScreen { } verify
  54. @Gio_Sastre fun ContentSubscriptionScreen ( @Composable subscriptions : ImmutableList<SubscriptionItem> , )

    { SubscriptionsLazyColumn ( items = subscriptions ) { … } BuySubscriptionButton ( } fun ErrorSubscriptionScreen ( @Composable ErrorButton () } fun LoadingSubscriptionScreen ( @Composable BuySubscriptionButton ( } ShimmerRows ( … ) enabled = false) enabled = true ) ) { fun ScaffoldSubscriptionScreen ( @Composable } Header ( … ) TopBar ( … ) Terms( … ) @Composable subscriptionsPlaceholder : ( ) -> Unit, subscriptionsPlaceholder( ) ScaffoldSubscriptionScreen { } ScaffoldSubscriptionScreen { } ScaffoldSubscriptionScreen { } verify
  55. @Gio_Sastre + CPDD TDD define UiStates assert UiStates + WHEN

    fetching at least 1 subscription, THEN show only the first one selected CPDD TDD
  56. @Gio_Sastre + CPDD TDD define UiStates assert UiStates + ScreenUiState.

    Content ImmutableList <SubscriptionItem> WHEN fetching at least 1 subscription, THEN show only the first one selected CPDD TDD
  57. @Gio_Sastre + CPDD TDD define UiStates assert UiStates + ScreenUiState.

    Content ImmutableList <SubscriptionItem> WHEN fetching at least 1 subscription, THEN show only the first one selected uiState. subscriptions. fi rst ( ) . selected uiState. subscriptions. fi lter { it.selected } . size == 1 CPDD TDD
  58. ). apply { CardViewHolder ( val layout = LayoutIn fl

    ater.from (context).in fl ate (R. card_layout, null) bind ( CardItem( ) ) }. itemView … container = layout @Gio_Sastre “Android View” Previews
  59. ). apply { CardViewHolder ( val layout = LayoutIn fl

    ater.from (context).in fl ate (R. card_layout, null) bind ( CardItem( ) ) }. itemView … container = layout @Gio_Sastre AndroidView ( modifier = Modifier.fillMaxSize() factory = { context -> ) } private fun CardViewHolderPreview() { @Composable @Preview CardViewHolderPreview “Android View” Previews
  60. ). apply { CardViewHolder ( val layout = LayoutIn fl

    ater.from (context).in fl ate (R. card_layout, null) bind ( CardItem( ) ) }. itemView … container = layout @Gio_Sastre AndroidView ( modifier = Modifier.fillMaxSize() factory = { context -> ) } private fun CardViewHolderPreview() { @Composable @Preview CardViewHolderPreview CardViewHolderPreview - Dark @Preview (name = “Dark”, uiMode = UI_MODE_NIGHT_YES) “Android View” Previews
  61. @Gio_Sastre - Single responsibility: 1 UI State -> 1 Composable

    Advantages of CPDD 1 @Composables code is understandable & maintainable 3 TDD & Testing behaviour becomes more intuitive - Defines Ui states you can assert in unit tests 2 Ensure no visual regression bugs in @Composables - Screenshot tests to automatically verify ALL other screens - @Previews for instant feedback on the current screen changes
  62. Linkedin @SergioSastre sergio-sastre @Gio_Sastre Hashnode blogs Sergio Sastre Flórez GDE

    for Android Lead Android developer appdev.de Github sergio-sastre-florez