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

Sizes Matter! - How to handle different screen ...

Sizes Matter! - How to handle different screen sizes on Android (using Jetpack Compose)

Nowadays, the variety of Android devices proliferates: smartphones, tablets, foldables, desktops, TVs, watches, even cars and we as Android developers are faced with the challenge of supporting all (or almost all) of these types of form factories.

android:screenOrientation="portrait"'s time is (or should be) over.

In this talk we will learn about window size classes, what they are, how they are defined and how they can help us solve the thorny problem of not knowing in advance the space on which an app will run.

Furthermore, there will be a live coding part of what I explained during the talk.

Avatar for Fabio Catinella

Fabio Catinella

April 26, 2025
Tweet

More Decks by Fabio Catinella

Other Decks in Programming

Transcript

  1. Fabio Catinella Senior Android Developer @ Var Group Sizes matter!

    How to handle di ff erent screen sizes on Android (using Jetpack Compose)
  2. Bad Reviews are around the corner Giving your users a

    bad experience on their expensive piece of tech, can really be a bad idea.
  3. App quality requirements are important Google sta rt ed to

    penalise apps that don’t meet minimum quality requirements. Source: h tt ps://android-developers.googleblog.com/2023/07/introducing-new- play-store-for-large-screens.html
  4. Large screen compatibility TIERS Source: h tt ps://developer.android.com/docs/quality-guidelines/large-screen- app-quality TIER

    3 (basic) Large screen ready Users can complete critical task fl ows but with a less than optimal user experience. Your app runs full screen (or full window in multi- window mode), but app layout might not be ideal. TIER 2 (be tt er) Large screen optimized Your app implements layout optimizations for all screen sizes and device con fi gurations along with enhanced suppo rt for external input devices. TIER 1 (best) Large screen di ff erentiated Your app provides a user experience designed for tablets, foldables, and ChromeOS devices. Where applicable, the app suppo rt s multitasking, foldable postures, drag and drop, and stylus input.
  5. Android 16+ If your app targets SDK 36+, the following

    manifest a tt ributes and APIs will be ignored on large screens (display >= 600dp) : Source: h tt ps://android-developers.googleblog.com/2025/01/orientation-and-resizability-changes-in-android-16.html
  6. Infinite Devices to support Over 3 Billion Active Devices Source:

    h tt ps://www.theverge.com/2021/5/18/22440813/android-devices-active- number-sma rt phones-google-2021
  7. Infinite Devices to support? Sma rt phones Foldables Tablet 0

    - 600 dp 600 - 840 dp +840 dp Window Size Classes
  8. Window Size Classes Rather than designing for an ever increasing

    number of display sizes, focusing on window size classes ensures layouts work across a range of devices. Compact Medium Expanded Source: h tt ps://developer.android.com/guide/topics/large-screens/suppo rt -di ff erent-screen-sizes
  9. Window Size Classes Window size classes categorize the display area

    available to your app as compact, medium, or expanded. Available width and height are classi fi ed separately, so at any point in time, your app has two window size classes—one for width, one for height.
  10. Window Size Classes Available width is usually more impo rt

    ant than available height due to the ubiquity of ve rt ical scrolling, so the width window size class is likely more relevant to your app's UI.
  11. TheGameDB App We want to show a proper UI when

    the app is running on a foldable.
  12. TheGameDB App We want to show a proper UI when

    the app is running on a foldable. Optimised Banner Larger Row
  13. Goals 1) We want to make our components adaptive 2)

    We want to display an optimized layout based on the current WindowWidthSizeClass To be sure to cover all the existing use cases we want to achieve two goals in our solution.
  14. Create adaptive components Taken a simple component: @Composable private fun

    HomeBannerElement( game: Game, modifier: Modifier = Modifier, ) { Card( modifier = modifier, shape = RoundedCornerShape(4.dp) ) { Box( modifier = Modifier .aspectRatio(0.8f) ) { . . . }
  15. private data class HomeBannerUiConfig( val cardAspectRatio: Float, val gameNameTextAlign: TextAlign,

    val gameDevelopmentCompanyTextAlign: TextAlign, val backgroundBrush: Brush, val textColumnHorizontalAlignment: Alignment.Horizontal, val columnAlignment: Alignment )
  16. Create adaptive components Taken a simple component: @Composable private fun

    HomeBannerElement( game: Game, modifier: Modifier = Modifier, ) { Card( modifier = modifier, shape = RoundedCornerShape(4.dp) ) { Box( modifier = Modifier .aspectRatio(0.8f) ) { . . . }
  17. Create adaptive components Taken a simple component: @Composable private fun

    HomeBannerElement( game: Game, modifier: Modifier = Modifier, ) { val uiConfig = remember { HomeBannerUiConfig() } Card( modifier = modifier, shape = RoundedCornerShape(4.dp) ) { Box( modifier = Modifier .aspectRatio(uiConfig.cardAspectRatio) ) { . . . }
  18. Create adaptive components @Composable private fun HomeBannerElement( game: Game, currentWindowWidthSizeClass:

    WindowWidthSizeClass, modifier: Modifier = Modifier, ) { val uiConfig = remember (currentWindowWidthSizeClass) { getUiConfig(currentWindowWidthSizeClass) } Card( modifier = modifier, shape = RoundedCornerShape(4.dp) ) { Box( modifier = Modifier .aspectRatio(uiConfig.cardAspectRatio) ) { . . . }
  19. Create adaptive components private fun getUiConfig(currentWindowWidthSizeClass: WindowWidthSizeClass): HomeBannerUiConfig = when

    (currentWindowWidthSizeClass) { WindowWidthSizeClass.Expanded -> { HomeBannerUiConfig( cardAspectRatio = 2.5f, gameNameTextAlign = TextAlign.Start, gameDevelopmentCompanyTextAlign = TextAlign.Start, backgroundBrush = Brush.horizontalGradient( colors = listOf( Color.Black.copy(alpha = 0.8f), Color.Transparent ), ), textColumnHorizontalAlignment = Alignment.Start ) } else -> { HomeBannerUiConfig( cardAspectRatio = 0.8f, gameNameTextAlign = TextAlign.Center, gameDevelopmentCompanyTextAlign = TextAlign.Center, backgroundBrush = Brush.linearGradient( colors = listOf( Color.Black.copy(alpha = 0.8f), Color.Transparent ), ), textColumnHorizontalAlignment = Alignment.CenterHorizontally ) } }
  20. Display an optimised layout Taken a simple app that uses

    navigation like this: Home Destination Details Destination MainActivity NavGraph
  21. Display an optimised layout Taken a simple app that uses

    navigation like this: Home Destination @Composable fun HomeDestination( homeViewModel: HomeViewModel = hiltViewModel(), navigator: Navigator, ) { val uiState by homeViewModel.uiState.collectAsStateWithLifecycle() Box( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.surfaceVariant) .windowInsetsPadding(WindowInsets.statusBars) ){. . . .} }
  22. Display an optimised layout We can extract the code that

    displays the UI in another component called HomeScreen Home Destination @Composable fun HomeDestination( homeViewModel: HomeViewModel = hiltViewModel(), navigator: Navigator, ) { val uiState by homeViewModel.uiState.collectAsStateWithLifecycle() HomeScreen( uiState = uiState ) } @Composable private fun HomeScreen( uiState: HomeUiState ){ Box( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.surfaceVariant) .windowInsetsPadding(WindowInsets.statusBars) ){. . . .} } HomeScreen
  23. Display an optimised layout We can de fi ne 3

    screens, one for each WindowWidthSizeClass and add logic to choose which to show Home Destination @Composable fun HomeDestination( homeViewModel: HomeViewModel = hiltViewModel(), currentWindowWidthSizeClass: WindowWidthSizeClass, navigator: Navigator, ) { val uiState by homeViewModel.uiState.collectAsStateWithLifecycle() when(currentWindowWidthSizeClass){ WindowWidthSizeClass.Expanded -> { HomeScreenExpanded( uiState = uiState, ) } WindowWidthSizeClass.Medium -> { HomeScreenMedium( uiState = uiState ) } WindowWidthSizeClass.Compact -> { HomeScreen( uiState = uiState ) } } } HomeScreenExpanded HomeScreenMedium HomeScreen
  24. Both solutions have something in common… @Composable private fun HomeBannerElement(

    game: Game, currentWindowWidthSizeClass: WindowWidthSizeClass, modifier: Modifier = Modifier, ) { . . . } @Composable fun HomeDestination( homeViewModel: HomeViewModel = hiltViewModel(), currentWindowWidthSizeClass: WindowWidthSizeClass, navigator: Navigator, ) { . . . }
  25. calculateWindowSizeClass Source: h tt ps://developer.android.com/guide/topics/large-screens/suppo rt -di ff erent-screen-sizes Material

    3 compose library o ff ers a convent way to get the current WindowSizeClass @ExperimentalMaterial3WindowSizeClassApi @Composable fun calculateWindowSizeClass(activity: Activity): WindowSizeClass { // Observe view configuration changes and recalculate the size class on each change. We can't // use Activity#onConfigurationChanged as this will sometimes fail to be called on different // API levels, hence why this function needs to be @Composable so we can observe the // ComposeView's configuration changes. LocalConfiguration.current val density = LocalDensity.current val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity) val size = with(density) { metrics.bounds.toComposeRect().size.toDpSize() } return WindowSizeClass.calculateFromSize(size) }
  26. calculateWindowSizeClass Source: h tt ps://developer.android.com/guide/topics/large-screens/suppo rt -di ff erent-screen-sizes Material

    3 compose library o ff ers a convent way to get the current WindowSizeClass @ExperimentalMaterial3WindowSizeClassApi @Composable fun calculateWindowSizeClass(activity: Activity): WindowSizeClass { // Observe view configuration changes and recalculate the size class on each change. We can't // use Activity#onConfigurationChanged as this will sometimes fail to be called on different // API levels, hence why this function needs to be @Composable so we can observe the // ComposeView's configuration changes. LocalConfiguration.current val density = LocalDensity.current val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity) val size = with(density) { metrics.bounds.toComposeRect().size.toDpSize() } return WindowSizeClass.calculateFromSize(size) }
  27. class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { val currentWindowWidthSizeClass = calculateWindowSizeClass(this).widthSizeClass TheGameDbTheme { TheGameDbApp(currentWindowWidthSizeClass) } } } }
  28. class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { val currentWindowWidthSizeClass = calculateWindowSizeClass(this).widthSizeClass TheGameDbTheme { TheGameDbApp(currentWindowWidthSizeClass) } } } }
  29. The Problem We are forced to propagate the current WindowSizeClass

    to components which don’t need it MainActivity Column Box WindowSizeAware Component currentWindowSizeClass currentWindowSizeClass currentWindowSizeClass
  30. A familiar example If you ever have created a new

    Compose project, the fi rst thing you get is this piece of code class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyApplicationTheme { // A surface container using the ‘primary' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.primary) { Greeting("Android") } } } } }
  31. A familiar example If you ever have created a new

    Compose project, the fi rst thing you get is this piece of code class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyApplicationTheme { // A surface container using the 'primary' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.primary) { Greeting("Android") } } } } }
  32. A familiar example If you ever have created a new

    Compose project, the fi rst thing you get is this piece of code MyApplicationTheme { // A surface container using the ‘primary' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.primary) { Greeting("Android") } }
  33. A familiar example If you ever have created a new

    Compose project, the fi rst thing you get is this piece of code //A surface container using the ‘primary' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.primary) { Greeting("Android") }
  34. @Composable fun MaterialTheme( colorScheme: ColorScheme = MaterialTheme.colorScheme, shapes: Shapes =

    MaterialTheme.shapes, typography: Typography = MaterialTheme.typography, content: @Composable () -> Unit ) { val rippleIndication = androidx.compose.material.ripple.rememberRipple() val selectionColors = rememberTextSelectionColors(colorScheme) CompositionLocalProvider( LocalColorScheme provides colorScheme, LocalIndication provides rippleIndication, androidx.compose.material.ripple.LocalRippleTheme provides MaterialRippleTheme, LocalShapes provides shapes, LocalTextSelectionColors provides selectionColors, LocalTypography provides typography, ) { ProvideTextStyle(value = typography.bodyLarge, content = content) } }
  35. Locally scoped data with CompositionLocal Source: h tt ps://developer.android.com/jetpack/compose/compositionlocal CompositionLocal

    is a tool for passing data down through the Composition implicitly. Row { Image(…) Column { Text(...) Text(…) } } val localNumber = compositionLocalOf<Int>{ 0 } Row Image Column Text Text localNumber.current = 0 localNumber.current = 0 localNumber.current = 0 localNumber.current = 0 localNumber.current = 0
  36. Locally scoped data with CompositionLocal Source: h tt ps://developer.android.com/jetpack/compose/compositionlocal CompositionLocal

    is a tool for passing data down through the Composition implicitly. Row Image Column Text Text CompositionLocal value localNumber.current = 0 localNumber.current = 42 localNumber.current = 0 localNumber.current = 42 localNumber.current = 42 Row { Image(…) CompositionLocalProvider( localNumber provides 42 ){ Column { Text(...) Text(…) } } } val localNumber = compositionLocalOf<Int>{ 0 }
  37. When we should use CompositionLocal “A key signal for using

    CompositionLocal is when the parameter is cross-cu tt ing and intermediate layers of implementation should not be aware it exists.” Don’t over use it ! Over using CompositionLocal can lead to very hard to debug code, since its impossible to determine the source of that value The current windowSizeClass meets completely this kind of condition. Source: h tt ps://developer.android.com/jetpack/compose/compositionlocal
  38. For our purposes CompositionLocal is perfect We can de fi

    ne a “LocalWindowWidthSizeClass” and use it in every composable we create. val LocalWindowWidthSizeClass = compositionLocalOf<WindowWidthSizeClass> { WindowWidthSizeClass.Compact } And now wrap our entire app into a CompositionLocalProvider node setContent { val windowWidthSizeClass = calculateWindowSizeClass(this).widthSizeClass CompositionLocalProvider(LocalWindowWidthSizeClass provides windowWidthSizeClass) { ….. } }
  39. Result example @Composable fun HomeRoute( homeViewModel: HomeViewModel = koinViewModel(), currentWindowWidthSizeClass:

    WindowWidthSizeClass = LocalWindowWidthSizeClass.current, navigator: DestinationsNavigator, ) { val uiState by homeViewModel.uiState.collectAsStateWithLifecycle() when (currentWindowWidthSizeClass) { WindowWidthSizeClass.Medium -> { HomeScreenMedium( uiState = uiState, onGamePressed = { game -> navigator.navigate(GameDetailsRouteDestination(gameId = game.id)) } ) } else -> { HomeScreen( uiState = uiState, onGamePressed = { game -> navigator.navigate(GameDetailsRouteDestination(gameId = game.id)) } ) } } }
  40. Conclusions • Handle all types of screen sizes is not

    trivial • WindowSizeClass comes in help • CompositionLocalOf helps us using WindowsSizeClasses in an easy way
  41. There’s More Recently Google has released a bunch of new

    apis that make creating adaptive layout even easier than what we learnt today.