[MUSIC PLAYING] ALEX VANYO: Hi, I'm Alex Vanyo, and today, I want to share with you the fundamentals of implementing an app for all screen sizes. This talk focuses on building new UI or a new app with Compose. This talk assumes you already have a working knowledge of Compose since we're going to showcase how it can simplify development.
If you are new to Compose, be sure to check out our guides to learn the fundamentals of Android's new UI toolkit. If you're curious about upgrading existing view-based UI for all screens, be sure to check out the "Update your app for the larger screen" talk. Android enjoys a diverse ecosystem of devices and platforms.
Beyond the traditional smartphone, there are tablets, foldables, watches, TVs, computers, and cars capable of running Android apps. Today, we're going to focus on building an Android app from scratch that can run on smartphones, foldables, tablets, or Chrome OS. Whether you're working on an existing app or creating a new one from scratch, there are an increasing number of devices to imagine your app running on, with screen sizes ranging from the smallest phones, to foldables, to the largest tablets and laptops.
To help ensure your device runs well on all these devices, there's an approach I want to share that allows you to simplify your development. Whenever a user launches your app, they are giving you some screen real estate to display something useful to them. On smartphones, it's pretty common to have the whole screen to work with, but the screen might be wider, shorter, or in different orientations.
On foldables or tablets it's more likely that the user might split the screen between your app and another to multitask. On foldables, there might even be more than one physical screen your app can run on. In a windowed environment, like Chrome OS, your app might be resized just like a website could be in a desktop browser.
Optimizing your app for all screens from the beginning can seem daunting, but don't worry. There's a common thread here across all sizes and device types to easily build responsive and adaptive UI. The most important detail throughout all these scenarios is the screen size currently available to your app.
If you can adapt your content to all of these cases and handle when they change, then you'll be providing a great user experience on current and future devices. As an example, we're going to take a look at the Now in Android app that collects articles, videos, and announcements for everything Android. This is an app built from the ground up in Compose following the best architecture practices and also showcases how to build UI for large screens.
There are three primary types of data Now in Android displays-- news resources, topics, and authors. Each of the screens interact with at least one of these types of data, displaying pieces of news, saving them for later, or choosing which topics or authors to follow. Each screen has different sets of designs based on window size classes, which in are an opinionated set of breakpoints to account for common classes of devices.
Since this app generally has vertical scrolling content, the width window size classes are most important to us. A compact screen width is whenever the screen size is narrower than 600 dp, which corresponds to the common case of smartphones in portrait mode. A medium width is a screen width of 600 dp up to 840 dp, which corresponds to the case of tablets and inner displays of foldables in portrait mode.
Finally, an expanded width is a screen width of 840 dp or more, which corresponds to tablets, and foldables in landscape mode, and desktop environments. All right, let's start making those designs a reality. Our first development topic will be what determines the first screen your user sees when they open the app-- navigation.
The top-level navigation for Now in Android consists of four destinations-- For You, Episodes, Saved, and Interests. At smaller screen widths, our designs have a navigation bar to navigate between these four destinations. When there is more space, at a medium width of 600 dp, we'll instead show a navigation rail on the side.
Placing the navigation elements here is more ergonomic for larger screens and makes better use of the rest of the screen. When there is even more space and an expanded width of 840 dp, we can show a navigation drawer. This is an example of adaptive UI, where we display different components at different screen sizes.
We'll later see examples of responsive UI as well, where we change the positioning or layout of the content instead. Now, let's talk about managing the navigation structure itself. Even though we're showing different controls, there's no need to change the navigation structure.
The destinations can remain the same across different screen sizes while we vary what content we display at each size. This allows us to maintain where we are in the app, even as the user rotates, resizes, or folds their device. Let's talk a bit about the alternative, which we do not recommend.
Depending on your designs, your app might look quite different at different screen sizes, especially with large screen-specific layouts, like a feed or list detail view. Because of this, an alternative idea would be to have two completely different navigation graphs-- one for phones and one for tablets. The biggest drawback to this approach is that you can't assume that isTablet is static.
When a user starts or ends multitasking, the screen size available to your app may shrink or grow. On a foldable device, the physical screen showing your app can change as the user unfolds or folds the device. In all these cases, you will want to seamlessly switch between your smaller and larger designs and keep the app state throughout these changes.
So you would have to map between the two different navigation graphs and switch between them as a side effect of the screen size changing. Therefore, a single navigation graph where the content displayed at each destination changes based on the screen size is our recommended approach to ensure the user state is preserved. Let's dive into some code now.
To implement navigation, we'll be using Navigation Compose. To start with, we'll set up our four top-level destinations in our navigation host. At each of these four routes, we'll be creating content for each of the destinations.
For right now, we'll just leave these as placeholders. Next, we'll wrap the navigation host with our navigation UI. Since our designs call for showing different UI based on the screen size available, we will choose which one to show based on the window size classes that align with our designs.
If the width window size class is compact, then we'll show the navigation bar. Otherwise, to the start of the navigation host, we'll show the navigation rail for medium sizes or a navigation drawer for expanded sizes. Putting it all together, we've set up our primary navigation, and we're ready to start showing some content at each destination.
For more information about the principles of navigation on Android and more information about Navigation Compose, take a look at these resources. Now, let's take a closer look at filling up one of those blank screens. We're going to focus on the For You destination and see how we approach managing the state for displaying content at different screen sizes.
Looking through the designs, the data required to display the content doesn't change much between the smallest design and the largest design. We might be showing more cards or change where they're positioned, but we can aim towards having the same set of data and state driving our UI. This matches the philosophy of unidirectional data flow, which is fundamental to Compose.
At different screen sizes, we choose between alternate ways to render the same data in interactions to the user in the space available to us. This also simplifies the logic for each screen since we don't have to create a different implementation for different screen sizes. Compose makes this easier than XML layouts because this diagram gets us almost all the way to our real code.
We can just directly use an if-else statement to change what UI we output based on the window size class state. With XML layouts, we'd have to deal with alternate versions based on resource qualifiers or manage hiding and removing sets of views at runtime. If you decided to try out Compose in your app, don't forget that interoperability with views means you can start to take advantage of Compose for some of these places where Compose's strengths are strongest, even without fully migrating.
Let's see what all that looks like in practice. We'll be putting most our state-handling code for the screen in a view model so that our state stays in memory during configuration changes. Our guiding principle here is that the UI state we produce is independent of the screen size, and it should be able to drive the content at any screen size.
In order to show the screen, we'll pull data from our repositories and combine this data into a single flow for our UI to collect. This UI state is a combination of data for the topics, which topics the user has selected, and the data for the news resources for those selected topics. In the view model, we also will expose methods that will allow the user to interact and change the underlying state, selecting topics, saving items, and so on.
These methods will translate user events into modifications to our repositories, which, in turn, will update the state displayed to the user to reflect their interactions. Over on the UI side, we'll set up what we display in the ForYouRoute(). The route is where we connect the view model to the UI and also pass down the additional information for what the current screen size is.
This allows ForYouScreen() composable to be stateless, and it will display UI based on the state passed to its arguments. Finally, in the ForYouScreen() itself, we transform our state into UI. We combine all the state from our parameters-- the data for the screen, as well as a window size class width.
Using both of these, we can choose to render a UI appropriately for the available size. Here's our example of responsive UI. We show two columns when we have an expanded width, and we only show one column when we have a compact or medium width, just like our designs.
The content remains the same, but where we're placing it on the screen is different for different screen sizes. For more information about architecting your app, check out the architecture docs for the different scenarios you might come across. Last but not least, let's talk about testing.
By following unidirectional data flow in how we create our UI for the screen, it's easy to test these pieces in isolation as well as altogether. When we're testing just the view model, we don't really care about the size of the screen. All that matters is that events from the user result in the state updates we'd expect.
To ensure you're following unidirectional data flow principles, you can even imagine using the single view model to drive the UI across a bunch of different devices at the same time. The same events will result in the same underlying state, but that state could result in different UI since each device is using the screen space available to it. For example, this test encodes a promise that when any UI updates the topic selection, the state will be updated correctly.
In UI tests, one very useful tool for testing configuration changes is the stateRestorationTester. By wrapping a composeTestRule, we can emulate saved instance state saving and restoring. Using this, we can easily check that any state preserved by rememberSavable is saved and restored properly, just like what happens upon process death or configuration change caused by rotating or folding.
Here, we have an example test ensuring that the amount incremented by a button is saved by instance state. Because the count is being remembered by rememberSaveable, the count stays at 1 after we emulate saved instance state restoration. Also on the UI side, let's look at tests for our ForYouScreen().
By hoisting the window size class, we can write tests for each of our screen sizes in isolation by directly specifying the size class parameter. For example, here, we have template tests for the three different width size classes. If we add tests for various states, we can verify that our UI is what we'd expect at each size class.
This hoisting also makes it easy to write previews for different screen sizes since we are explicitly declaring which size class to use. With this setup, you'll be able to see how changes impact each screen configuration in the preview window in Android Studio. Finally, for larger integration tests, we can specify device requirements for situations where UI will only be shown when the screen is large enough by defining an annotation.
We can then use this annotation to run subsets of tests. So we don't try to run tests on a small phone if we know we need a device with a screen large enough to run at an expanded width. Since supporting more screen sizes means needing to support more devices, we're also improving testing tools to make it easier to maintain adaptive and responsive UIs.
You can use the Virtual Device Manager in Android Studio to create a variety of emulators to run these tests. There are smartphone, foldable, and tablet images available. And if you can't decide, we've got you covered with the new resizable emulator coming soon to Android Studio to allow switching between all of them.
If you have multiple emulators running, you can also run your tests across all of them simultaneously and see the results on each device in the test matrix. Additionally, Gradle Managed Devices will make automated testing with virtual devices easier. Available soon in Android Studio, you can configure specific device profiles to run tests against, with setup, deployment, running, and teardown all handled directly by the Android Gradle plugin.
As you develop your application, we encourage you to test across a variety of device types to ensure your app works great on all of them. I hope this guide has been useful for our approach for building an app for all screen sizes. As you're adding a feature or structuring your UI, keep in mind how best to display content in the space available to your app.
A public sample for Now in Android is coming soon, so stay tuned if you're curious about what this looks like in practice in a complete app. For even more large-screen content, "Update your app for the larger screen" goes into more detail on how to add more large-screen support to your existing apps. Thank you, and I'm excited to see which device your app runs on next.