Craft Adaptable UI for Android Applications Using Jetpack Compose

Samiul Hossain

Engineer, Samsung Developer Program

In the dynamic landscape of mobile technology, the introduction of the Jetpack Compose toolkit has opened a lot of opportunities for developers to create beautiful, seamless applications using declarative UI. Using this new model of UI development, developers can create adaptable applications targeting a wide range of mobile devices.

In this post, we learn how to integrate Android's new adaptive library into a pre-built compose application and leverage its APIs to create a dynamic user interface.

Overview of the application

undefined
undefined
undefined
undefined

Figure 1: Application UI on the Galaxy Z Flip5

undefined

The example application is a simple list of mobile devices for sale. It is built using an ElevatedCard composable that is displayed by a LazyVerticalGrid composable. Each card is modeled after a data class named Mobile. Let’s take a look at the data class and composable functions below:

/// Data class to hold Mobile data
data class Mobile(
    @StringRes val name: Int,
    @DrawableRes val photoId: Int,
    val price: String
)
/// MainActivity.kt

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeAppTheme {
                MyApp()
            }
        }
    }
}

@Composable
fun MyApp(){
    Surface(
        modifier = Modifier
            .fillMaxSize()
            .statusBarsPadding(),
        color = MaterialTheme.colorScheme.background
    ) {
        MobileGrid(
            modifier = Modifier.padding(
                start = 8.dp,
                top = 8.dp,
                end = 8.dp,
            )
        )
    }
}

@Composable
fun MobileGrid(modifier: Modifier = Modifier){
    LazyVerticalGrid(
        columns = GridCells.Fixed(2),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        modifier = modifier
    ) {
        items(MobileDataSource.mobiles) { mobile ->
            MobileCard(mobile)
        }
    }
}

@Composable
fun MobileCard(mobile: Mobile, modifier: Modifier=Modifier){
    ElevatedCard() {
        Row {
            Image(
                painter = painterResource(id = mobile.photoId),
                contentDescription = null,
                modifier = modifier
                    .size(width = 68.dp, height = 68.dp),
                contentScale = ContentScale.Crop
            )
            Column(
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center
            ) {
                Text(
                    text = stringResource(id = mobile.name),
                    modifier = Modifier.padding(
                        start = 16.dp,
                        top = 16.dp,
                        end = 16.dp,
                    ),
                    style = MaterialTheme.typography.labelLarge,
                )
                Text(
                    text = mobile.price,
                    style = MaterialTheme.typography.labelSmall,
                )
            }
        }
    }
}

As we’ve seen, the application UI looks good on the Samsung Galaxy Z Flip5. But how does it look on the Galaxy Z Fold5?

undefined
undefined
undefined
undefined

Figure 2: Application UI on the Galaxy Z Fold5

undefined

On the Galaxy Z Fold5, the cards are now very stretched and contain a lot of blank space. The unfolded state of foldable devices has a larger screen size and it is important to keep large screens in mind when developing your application. Otherwise, the application may look great on conventional mobile devices, but very off putting on larger devices such as tablets, foldables, and so on.

Create an adaptive layout for your application

The material-3 adaptive library provides some top-level functions that we can leverage to adapt our applications to different form factors. We will use the currentWindowAdaptiveInfo() function to retrieve the WindowSizeClass. The WindowSizeClass allows us to catch breakpoints in the viewport and change the application UI for different form factors. Follow the steps below to change the application's appearance depending on the screen size.

  1. Add the following dependencies to the app-level build.grade file
     ...
     implementation "androidx.compose.material3.adaptive:adaptive:1.0.0-beta04"
     ...
    

  2. Create a variable called windowSizeClass to store the WindowSizeClass from currentWindowAdaptiveInfo() in the MobileGrid() composable. It contains a member variable named widthSizeClass that is a type of WindowWidthSizeClass. The possible values of this class are Compact, Medium, and Expanded. We will use this value to change the layout of the application. Create a new variable named numberOfColumns to dynamically set the number of grid columns in the MobileGrid() composable depending on the width of the screen.
    fun MobileGrid(modifier: Modifier = Modifier){
        val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
        val numberOfColumns: Int = when(windowSizeClass.windowWidthSizeClass) {
        WindowWidthSizeClass.COMPACT -> 2
        WindowWidthSizeClass.MEDIUM -> 3
        else -> 4
    }
    
        LazyVerticalGrid(
            modifier = modifier,
            columns = GridCells.Fixed(numberOfColumns),
            verticalArrangement = Arrangement.spacedBy(8.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            items(MobileDataSource.mobiles) { mobile ->
                MobileCard(mobile)
            }
        }
    }
    

That's all! Your application now has a seamless, responsive UI that changes based on the size of the screen it is being displayed on. Let's see what it looks like now on the Galaxy Z Fold5.

undefined
undefined
undefined
undefined

Figure 3: Updated UI on the Galaxy Z Fold5

undefined

Add support for pop-up view

Android enables users to improve their efficiency by leveraging its multi-tasking features. More than half of foldable users use the split-screen, multi window, or pop-up modes daily, so it is imperative that modern applications integrate support for these viewing modes. Let's have a look at the UI in pop-up mode.

undefined
undefined
undefined
undefined

Figure 4: UI on the Galaxy Z Fold5 - pop-up mode

undefined

As you can see, the UI is completely broken in pop-up mode. The mode has a much smaller viewport width and height, so it'd be better to display just 1 column of tiles. We can do this by using the currentWindowSize() function from the adaptive library that uses the WindowMetrics class to calculate the width and height of the viewport. Create a variable named currentWindowWidthSize and retrieve the window width size using the function. If the viewport width is too low, less than 800 pixels in the example below, we can set the numberOfColumns variable to 1.

@Composable
    fun MobileGrid(modifier: Modifier = Modifier){
        val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
        val currentWindowWidthSize = currentWindowSize().width
        val numberOfColumns: Int = when(windowSizeClass.windowWidthSizeClass) {
            WindowWidthSizeClass.COMPACT -> {
                if(currentWindowWidthSize < 800) 1 else 2
            }
            WindowWidthSizeClass.MEDIUM -> 3
            else -> 4
        }

        LazyVerticalGrid(
            modifier = modifier,
            columns = GridCells.Fixed(numberOfColumns),
            verticalArrangement = Arrangement.spacedBy(8.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            items(MobileDataSource.mobiles) { mobile ->
                MobileCard(mobile)
            }
        }
    }
undefined
undefined
undefined
undefined

Figure 5: Updated UI on the Galaxy Z Fold5 - pop-up mode

undefined

Conclusion

You have now successfully used the new material-3 adaptive library to change the layout of your application to support foldables and large screen devices in portrait, landscape, split-screen or pop-up modes. By leveraging Jetpack Compose and Android APIs, you can create a consistent and optimized user experience across various screen sizes and device types. If you are interested in developing adaptive applications in XML, check out the links in the section below.