[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 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 the window size classes, which
are an opinionated set of break points
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 at 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 [? this ?] tablet 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's 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 NavigationUI.
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, 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 are 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're excited to try out composing 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 of 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 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 ViewModel, 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 ViewModel to the UI
and also pass down the additional information for what
the current screen size is.
This allows the 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
and 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 in just the ViewModel,
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 a single ViewModel
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 instant 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 count incremented by a button
is saved by instance state.
Because the count is being remembered by rememberSavable,
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 tear down 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 a 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.
[MUSIC PLAYING]