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

Mastering Adaptive Layouts: Visual Testing for ...

Mastering Adaptive Layouts: Visual Testing for Every Screen

Android’s device ecosystem has expanded beyond phones - from foldable devices and tablets to Android TVs. Building truly adaptive layouts needs more than responsive design. It requires comprehensive visual testing to ensure that the UI works on every screen.

In this talk, we will discover how to build visual test suites that automatically validate your layouts across different screen sizes, densities, and aspect ratios. We will also discuss how frameworks for screenshot tests differ from each other and which tools can improve the visual testing process.

Avatar for Alex Zhukovich

Alex Zhukovich

September 04, 2025
Tweet

More Decks by Alex Zhukovich

Other Decks in Technology

Transcript

  1. Misaligned components & spacing inconsistencies Incorrect colors for color modes

    & custom themes Broken layouts for different devices Localization problems Accessibility issues related to displaying content Unexpected rendering problems in speci fi
  2. Misaligned components & spacing inconsistencies Incorrect colors for color modes

    & custom themes Broken layouts for different devices Localization problems Accessibility issues related to displaying content Unexpected rendering problems in speci fi
  3. Misaligned components & spacing inconsistencies Incorrect colors for color modes

    & custom themes Broken layouts for different devices Localization problems Accessibility issues related to displaying content Unexpected rendering problems in speci fi
  4. Misaligned components & spacing inconsistencies Incorrect colors for color modes

    & custom themes Broken layouts for different devices Localization problems Accessibility issues related to displaying content Unexpected rendering problems in speci fi
  5. Misaligned components & spacing inconsistencies Incorrect colors for color modes

    & custom themes Broken layouts for different devices Localization problems Accessibility issues related to displaying content Unexpected rendering problems in speci fi
  6. Misaligned components & spacing inconsistencies Incorrect colors for color modes

    & custom themes Broken layouts for different devices Localization problems Accessibility issues related to displaying content Unexpected rendering problems in speci fi c scenarios
  7. RENDERING METHOD TYPE OF TESTS COMPOSE PREVIEW SCREENSHOT TESTING ANDROID

    TESTIFY DROPSHOTS ROBORAZZI PAPARAZZI SHOT INSTRUMENTATION DEVICE NATIVE RENDERING DEVICE NATIVE RENDERING DEVICE NATIVE RENDERING INSTRUMENTATION INSTRUMENTATION
  8. RENDERING METHOD TYPE OF TESTS COMPOSE PREVIEW SCREENSHOT TESTING ANDROID

    TESTIFY DROPSHOTS ROBORAZZI PAPARAZZI SHOT INSTRUMENTATION LAYOUTLIB LOCAL DEVICE NATIVE RENDERING LAYOUTLIB DEVICE NATIVE RENDERING DEVICE NATIVE RENDERING INSTRUMENTATION LOCAL INSTRUMENTATION
  9. RENDERING METHOD TYPE OF TESTS COMPOSE PREVIEW SCREENSHOT TESTING ANDROID

    TESTIFY DROPSHOTS ROBORAZZI PAPARAZZI SHOT INSTRUMENTATION LAYOUTLIB LOCAL DEVICE NATIVE RENDERING LAYOUTLIB ROBOLECTRIC NATIVE GRAPHICS DEVICE NATIVE RENDERING DEVICE NATIVE RENDERING INSTRUMENTATION LOCAL LOCAL INSTRUMENTATION
  10. RENDERING METHOD CAN INTERACT WITH UI COMPOSE PREVIEW SCREENSHOT TESTING

    ANDROID TESTIFY DROPSHOTS ROBORAZZI PAPARAZZI SHOT YES LAYOUTLIB DEVICE NATIVE RENDERING LAYOUTLIB ROBOLECTRIC NATIVE GRAPHICS DEVICE NATIVE RENDERING DEVICE NATIVE RENDERING YES YES YES
  11. SUPPORT JETPACK COMPOSE SUPPORT VIEW COMPOSE PREVIEW SCREENSHOT TESTING ANDROID

    TESTIFY DROPSHOTS ROBORAZZI PAPARAZZI SHOT YES YES YES YES YES YES YES YES YES YES YES
  12. Find previews in the entire project Support all rendering methods

    Previews should be located in the “screenshotTest” source set Use “layoutlib” COMPOSE PREVIEW SCREENSHOT TESTING COMPOSE PREVIEW SCANNER
  13. COMPOSE PREVIEW SCREENSHOT TESTING @Preview @Composable fun MoodListItemPreview() { AppTheme

    { MoodListItem( mood = Mood.HAPPY, time = LocalTime.of(13, 15), activities = "Study", note = "Super important note", onClick = {} ) } }
  14. ROBORAZZI @RunWith(RobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class MoodListItemScreenshotTest { @get:Rule val composeTestRule =

    createComposeRule() @Test fun moodItemList_happy_withActivityAndNote() { composeTestRule.setContent { AppTheme { MoodListItem( mood = Mood.HAPPY, time = LocalTime.of(13, 15), activities = "Study", note = "Super important note", onClick = {} ) } } composeTestRule .onRoot() .captureRoboImage() } }
  15. SHOT class MoodListItemScreenshotTest : ScreenshotTest { @get:Rule val composeTestRule =

    createComposeRule() @Test fun moodItemList_happy_withActivityAndNote() { composeTestRule.setContent { AppTheme { MoodListItem( mood = Mood.HAPPY, time = LocalTime.of(13, 15), activities = "Study", note = "Super important note", onClick = {} ) } } compareScreenshot(composeTestRule) } }
  16. ROBORAZZI COMPOSE PREVIEW COLOR MODES @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode =

    Configuration.UI_MODE_NIGHT_NO) @Composable fun MoodListItemPreview() { AppTheme { MoodListItem( mood = Mood.HAPPY, time = LocalTime.of(13, 15), activities = "Study", note = "Super important note", onClick = {} ) } } @RunWith(RobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class MoodListItemScreenshotTest { @get:Rule val composeTestRule = createComposeRule() @Test @Config(qualifiers = "night") fun moodItemList_happy_withActivityAndNote() { composeTestRule.setContent { AppTheme { MoodListItem( mood = Mood.HAPPY, time = LocalTime.of(13, 15), activities = "Study", note = "Super important note", onClick = {} ) } } composeTestRule .onRoot() .captureRoboImage() } }
  17. ROBORAZZI COMPOSE PREVIEW COLOR MODES @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode =

    Configuration.UI_MODE_NIGHT_NO) @Composable fun MoodListItemPreview() { AppTheme { MoodListItem( mood = Mood.HAPPY, time = LocalTime.of(13, 15), activities = "Study", note = "Super important note", onClick = {} ) } } @RunWith(RobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class MoodListItemScreenshotTest { @get:Rule val composeTestRule = createComposeRule() @Test @Config(qualifiers = "night") fun moodItemList_happy_withActivityAndNote() { composeTestRule.setContent { AppTheme { MoodListItem( mood = Mood.HAPPY, time = LocalTime.of(13, 15), activities = "Study", note = "Super important note", onClick = {} ) } } composeTestRule .onRoot() .captureRoboImage() } }
  18. ROBORAZZI COMPOSE PREVIEW FONT SIZES @Preview(fontScale = 1.3f) @Composable fun

    MoodListItemPreview() { AppTheme { MoodListItem( mood = Mood.HAPPY, time = LocalTime.of(13, 15), activities = "Study", note = "Super important note", onClick = {} ) } } @RunWith(RobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class MoodListItemScreenshotTest { @get:Rule val composeTestRule = createComposeRule() @Test @Config(fontScale = 0.85f) fun moodItemList_happy_withActivityAndNote() { composeTestRule.setContent { AppTheme { MoodListItem( mood = Mood.HAPPY, time = LocalTime.of(13, 15), activities = "Study", note = "Super important note", onClick = {} ) } } composeTestRule .onRoot() .captureRoboImage() } }
  19. ROBORAZZI COMPOSE PREVIEW FONT SIZES @Preview(fontScale = 1.3f) @Composable fun

    MoodListItemPreview() { AppTheme { MoodListItem( mood = Mood.HAPPY, time = LocalTime.of(13, 15), activities = "Study", note = "Super important note", onClick = {} ) } } @RunWith(RobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class MoodListItemScreenshotTest { @get:Rule val composeTestRule = createComposeRule() @Test @Config(fontScale = 0.85f) fun moodItemList_happy_withActivityAndNote() { composeTestRule.setContent { AppTheme { MoodListItem( mood = Mood.HAPPY, time = LocalTime.of(13, 15), activities = "Study", note = "Super important note", onClick = {} ) } } composeTestRule .onRoot() .captureRoboImage() } }
  20. ROBORAZZI COMPOSE PREVIEW LOCALES @Preview(locale = "ar") @Composable fun MoodListItemPreview()

    { AppTheme { MoodListItem( mood = Mood.HAPPY, time = LocalTime.of(13, 15), activities = "Study", note = "Super important note", onClick = {} ) } } @RunWith(RobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class MoodListItemScreenshotTest { @get:Rule val composeTestRule = createComposeRule() @Test @Config(qualifiers = "ar") fun moodItemList_happy_withActivityAndNote() { composeTestRule.setContent { AppTheme { MoodListItem( mood = Mood.HAPPY, time = LocalTime.of(13, 15), activities = "Study", note = "Super important note", onClick = {} ) } } composeTestRule .onRoot() .captureRoboImage() } }
  21. ROBORAZZI COMPOSE PREVIEW LOCALES @Preview(locale = "ar") @Composable fun MoodListItemPreview()

    { AppTheme { MoodListItem( mood = Mood.HAPPY, time = LocalTime.of(13, 15), activities = "Study", note = "Super important note", onClick = {} ) } } @RunWith(RobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class MoodListItemScreenshotTest { @get:Rule val composeTestRule = createComposeRule() @Test @Config(qualifiers = "ar") fun moodItemList_happy_withActivityAndNote() { composeTestRule.setContent { AppTheme { MoodListItem( mood = Mood.HAPPY, time = LocalTime.of(13, 15), activities = "Study", note = "Super important note", onClick = {} ) } } composeTestRule .onRoot() .captureRoboImage() } }
  22. ROBORAZZI DEVICE TYPES @RunWith(RobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class MoodListItemScreenshotTest { @get:Rule val

    composeTestRule = createComposeRule() @Test @Config(qualifiers = "w1024dp-h600dp-land-xhdpi") fun manageCategories_tablet_landscape() { val uiState = ActionCategoriesScreenUiState(...) composeTestRule.setContent { AppTheme { ActionCategoriesAdaptiveScreenContent( uiState = uiState, onNavigateUp = {}, ... ) } } composeTestRule .onRoot() .captureRoboImage() } }
  23. PARAMETERIZED TESTS class DateRangeSelectorScreenshotTest : ScreenshotTest { @get:Rule val composeTestRule

    = createEmptyComposeRule() @Test fun dateRangeSelector_selectedTodayDate() { val date = LocalDate.of(2025, 1, 15) val activityScenario = ActivityScenarioConfigurator.ForComposable() .setUiMode(UiMode.NIGHT) .setFontSize(FontSize.LARGE) .launchConfiguredActivity() .onActivity { it.setContent { val state = rememberDateRangeSelectorState( currentDate = date, selectedDate = date, daysCount = 7 ) AppTheme { DateRangeSelector(state = state) } } } activityScenario.waitForActivity() compareScreenshot( rule = composeTestRule, name = "dateRangeSelector_night_large" ) activityScenario.close() } }
  24. PARAMETERIZED TESTS @RunWith(TestParameterInjector::class) class DateRangeSelectorScreenshotTest : ScreenshotTest { @get:Rule val

    composeTestRule = createEmptyComposeRule() @Test fun dateRangeSelector_selectedTodayDate( @TestParameter uiMode: UiMode, @TestParameter fontSize: FontSize ) { val date = LocalDate.of(2025, 1, 15) val activityScenario = ActivityScenarioConfigurator.ForComposable() .setUiMode(uiMode) .setFontSize(fontSize) .launchConfiguredActivity() .onActivity { it.setContent { val state = rememberDateRangeSelectorState( currentDate = date, selectedDate = date, daysCount = 7 ) AppTheme { DateRangeSelector(state = state) } } } activityScenario.waitForActivity() compareScreenshot( rule = composeTestRule, name = "dateRangeSelector_${uiMode.name.lowercase()}_${fontSize.name.lowercase()}" ) activityScenario.close() } }
  25. HOW TO HANDLE FLAKINESS IN VISUAL TESTS Use Static Data

    Avoid Data Sources (Network, DB) Avoid tolerance when possible