Building a Responsive UI in Android
There’s few things more annoying than realizing the great looking screenshots from Google Play don’t translate to your device. I certainly don’t expect every app to test their UI on the 11,000+ Android devices out there and, thankfully, you don’t have to if you’re building a responsive UI.
More so than most parts of Android development, building a responsive UI is a multi-disciplinary act: everyone will need to work towards deciding when and how to change your UI.
Choosing when to change
A responsive UI is simply a UI that reacts to the amount of available space. While it may be tempting to think about just your device or a specific phone or tablet, Android takes that “be together not the same” motto to heart: it is a continuous spectrum of devices and sizes (something that web and desktop developers have had to deal with for years!).
The Android resource system gives you the tools to provide alternate resources based on the available width, height, or smallest width — important measurements that serve as the basis for choosing when to change our UI based on the space available.
Width and Breakpoints
Width is perhaps the most important dimension when it comes to choosing when to change your UI. This is because width is the basis for the breakpoint system.
A ‘breakpoint’ is a specific width where your UI could differ: respond to the new additional space to better take advantage of it. It could be a small difference (slightly larger margins) or a much larger change — see the ‘what to change’ section below for plenty of examples.
A good example of this is the 600dp line: it is only at this point when you should consider having two levels of content hierarchy (say a master and detail view) on the screen at the same time. Below this point and you have a very real possibility to overwhelm the user.
Similarly, the extremely larger sizes after 1600dp is a good time to set a maximum width for your UI: either center aligning and growing the margins or left aligning your entire UI.
Note: this isn’t to say your layout is completely static and fixed width between each breakpoint. Think really hard on what elements really truly need a fixed width or which should be aligned with a more flexible grid, using a fixed percentage of the available space.
To take advantage of width-based resources, you’d use the w prefix. For example, a default layout would be a layout and a layout for width 600dp or higher would be in layout-w600dp.
Height, on the other hand, is less common as a high level element in building a responsive UI, but I wouldn’t count it out entirely. In fact, cases where the height is extremely limited is one of the cases where you UI can break down quite spectacularly.
Take for instance the case of a 16:9 split-screen window on mobile: such a small height can make traditionally easy to use interfaces (such as a vertically-scrolling container) into a difficult proposition.
Take particular care when using UI patterns that have fixed elements vertically aligned; while a dialog might work great in most cases, a small height might make it impossible to tap actions and interact with the dialog. (A good example where you might transform to a full screen dialog model).
Similar to qualifying resources by width, height aptly uses the h prefix — i.e., layout-h480dp.
Smallest Width and Rotation-Insensitive UIs
There’s a natural tendency to think about ‘available space’ based on the general size of the device (this is why it is so easy to fall into the ‘let’s build a tablet UI!’ thinking). But neither width or height actually cover that overall amount of size thinking — they each measure just a single dimension. But fret not! There’s an alternative to layout-w600dp-h600dp: smallest width and layout-sw600dp.
Smallest width is calculated by taking the smaller of the width and height. For example, if your app is in portrait orientation, the smallest width is the current width. When rotated, the smallest width doesn’t change even though the width has swapped with the height.
This makes smallest width a great overall representation of how much space is available. It is also really important when building a rotation insensitive UI. This means ensuring that operations are available in every orientation and that basic usage patterns are consistent across rotation.
This is particularly important when Preparing for Multi-Window in Android N:
That’s right: even if the device is in landscape, your app might be in ‘portrait’ orientation. Turns out: “portrait” really just means the height is greater than the width and “landscape” means the width is greater than the height … your app could transition from one to the other while being resized.
If you’re relying on portrait or landscape to change your UI, you and your users will be quite surprised when they resize your app and get to that magical cross over point between portrait and landscape. Make sure they aren’t surprised.
Of course, that doesn’t mean that nothing can change between orientations (there is a big difference in width after all!), but that you should keep larger structural/navigation changes to be based on smallest width if those kinds of sweeping changes are needed at all.
Choosing what to change
The ‘when’ to change is only half of the equation though. Knowing what (and how) to change is just as important.
There’s a number of responsive UI patterns which explore how you might adapt to a change in the amount of space you have.
The first pattern involves revealing hidden content when you gain additional space. This pattern exemplifies the balance between the needs of the small screen (there comes a point where not everything will fit!) with the needs of the large screen where reducing the number of taps or reliance on hidden information can greatly improve the user experience.
This is particularly common when it comes to layouts. On a smaller screen, specific user interaction (such as tapping a button) might be required to expand less used fields.
On a larger screen though, all fields might be shown by default.
How: Create two layouts with the same name, one in the layout directory and one in a layout-w600dp. Ensure that any findById() calls check for null values for Views that only appear in one of the layouts.
Transforming involves not changing what elements are being displayed, but instead changing what format or style they use. The simplest example is a menu where you have elements with showAsAction=”ifRoom” — when there’s additional space, elements would graduate from an overflow menu to toolbar items — transforming in style from text to icon.
Another example is in transforming how you display collections of data.
A simple scannable list might work great with a constrained width, but can be a difficult expand that style to a larger width. Converting that same collection into a grid can be an effective to use the space and put content first.
How: Take advantage of RecyclerView’s layoutManager attribute to change from a LinearLayoutManager to a GridLayoutManager without changing any of your code. Ensure that the View created in onCreateViewHolder() also changes at the same breakpoint as your layoutManager.
When you divide your screen, you use your additional space to display multiple pieces of your UI at the same time. This is a close cousin to the reveal pattern, but focuses more around changing the high level elements and navigation to segment the screen rather than only showing elements in existing containers.
A great place to use this pattern is when you have multiple tabs that are, by themselves, fairly simple.
Instead of applying a different pattern to each (now much larger) individual tab, you can divide the screen and display each piece of content in its entirety.
How: On smaller devices, you’d build your UI using Tabs and ViewPager, preferably with a Fragment for each page. Preferably based on smallest width (remember you want it consistent across rotations), use a layout that directly adds each individual Fragment.
Reflowing relies on the flexibility of the underlying pieces of your UI and can be done at the micro or macro level. For example, a single View might stack its content vertically when width is at a premium, but stack horizontally if more width is available.
The same concept applies to your entire UI though: it might make sense to reflow all of the Views to fill all the available space.
Views arranged in a single column on smaller devices may reflow to fill multiple columns.
Note: we’ve talked a lot about making layouts responsive by using width, height, and smallest width, but this same technique applies to all resource types. Instead of building separate XML files just to change the spanCount, make it an integer resource and provide an alternate value in the values-w600dp resource folder.
Not every change needs to be a large sweeping change: sometimes small changes are all you need. Simply expanding the space available to your UI (or adding some additional margin) can make for an effective way to change your UI.
How: as mentioned above, any resource can be responsive to changes in width, height, and smallest width. This type of expanding is actually easier done by adding margin: you could imagine a margin of 0dp for the smaller screen and 24dp on larger devices. Another solution would be to use the Percent Support Library, changing your layout_widthPercent from 100% to 80%, for example. If you’re instead looking for setting a max width on the entire View, consider reading this pro-tip on optimizing line length and its included MaxWidthLinearLayout.
Last but not least, changing the position of important Views can make all the difference in making an approachable UI.
Consider the FloatingActionButton (FAB). By definition, this should be tied to one of the primary actions the user would want to take. Particularly if you are changing other elements, consider following the guidelines for using the floating action button on large screens.
This may mean moving the floating action button from its ‘default’ location of the lower right to be attached to an extended app bar or attached to a toolbar or sheet within your layout itself.
How: Assuming you’re using a FloatingActionButton with a CoordinatorLayout, the lower right with layout_gravity=”bottom|end”, while you’d use layout_anchor with your extended height AppBarLayout and a layout_anchorGravity=”bottom|end” to tie the FAB to the AppBarLayout as explained in the Dependencies between Views in CoordinatorLayout blog post.
Go forth and build responsively
The goal of responsive design is to build a UI that looks great everywhere. But that’s an end goal: many of these patterns can be introduced very incrementally so don’t wait. (A little margin goes a long way!)
Follow the Android Development Patterns Collection for more!