Android provides all the ingredients for five-star large screen apps. The recipes in this cookbook select and combine choice ingredients to solve specific development problems. Each recipe includes best practices, quality code samples, and step-by-step instructions to help you become a large screen top chef.
Star ratings
The recipes are star rated based on how well they align with the Large screen app quality guidelines.
Meets the criteria for Tier 1, Large screen differentiated | |
Meets the criteria for Tier 2, Large screen optimized | |
Meets the criteria for Tier 3, Large screen ready | |
Provides some large screen capabilities, but falls short of the large screen app quality guidelines | |
Meets the needs of a specific use case, but doesn't properly support large screens |
Chromebook camera support
Get noticed on Google Play by Chromebook users.
If your camera app can function with only basic camera features, don't let app stores prevent Chromebook users from installing the app just because you inadvertently specified advanced camera features found on high-end phones.
Chromebooks have a built-in front (user-facing) camera that works well for video conferencing, snapshots, and other applications. But not all Chromebooks have a back (world-facing) camera, and most user-facing cameras on Chromebooks don't support autofocus or flash.
Best practices
Versatile camera apps support all devices regardless of camera configuration—devices with front cameras, back cameras, external cameras connected by USB.
To ensure apps stores make your app available to the greatest number of devices, always declare all camera features used by your app and explicitly indicate whether or not the features are required.
Ingredients
CAMERA
permission: Gives your app access to a device's cameras<uses-feature>
manifest element: Informs app stores of the features used by your apprequired
attribute: Indicates to app stores whether your app can function without a specified feature
Steps
Summary
Declare the CAMERA
permission. Declare camera features that provide basic camera support. Specify whether or not each feature is required.
1. Declare the CAMERA
permission
Add the following permission to the app manifest:
<uses-permission android:name="android.permission.CAMERA" />
2. Declare basic camera features
Add the following features to the app manifest:
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<uses-feature android:name="android.hardware.camera.flash" android:required="false" />
3. Specify whether each feature is required
Set android:required="false"
for the android.hardware.camera.any
feature to enable access to your app by devices that have any kind of built-in or external camera—or no camera at all.
For the other features, set android:required="false"
to ensure devices such as Chromebooks that don't have back cameras, autofocus, or flash can access your app on app stores.
Results
Chromebook users can download and install your app from Google Play and other app stores. And devices with full‑featured camera support, like phones, won't be restricted in their camera functionality.
By explicitly setting the camera features supported by your app and specifying the features your app requires, you've made your app available to as many devices as possible.
Additional resources
For more information, see Camera hardware features in the <uses-feature>
documentation.
App orientation restricted on phones but not on large screen devices
Your app works great on phones in portrait orientation, so you've restricted the app to portrait only. But you see an opportunity to do more on large screens in landscape orientation.
How can you have it both ways—restrict the app to portrait orientation on small screens, but enable landscape on large?
Best practices
The best apps respect user preferences such as device orientation.
The Large screen app quality guidelines recommend that apps support all device configurations, including portrait and landscape orientations, multi-window mode, and folded and unfolded states of foldable devices. Apps should optimize layouts and user interfaces for different configurations, and apps should save and restore state during configuration changes.
This recipe is a temporary measure—a pinch of large screen support. Use the recipe until you can improve your app to provide full support for all device configurations.
Ingredients
screenOrientation
: App manifest setting that enables you to specify how your app responds to device orientation changes- Jetpack WindowManager: Set of libraries that enable you to determine the size and aspect ratio of the app window; backward compatible to API level 14
Activity#setRequestedOrientation()
: Method with which you can change the app orientation at runtime
Steps
Summary
Enable the app to handle orientation changes by default in the app manifest. At runtime, determine the app window size. If the app window is small, restrict the app's orientation by overriding the manifest orientation setting.
1. Specify orientation setting in the app manifest
You can either avoid declaring the screenOrientation
element of the app manifest (in which case orientation defaults to unspecified
) or set screen orientation to fullUser
. If the user has not locked sensor-based rotation, your app will support all device orientations.
<activity
android:name=".MyActivity"
android:screenOrientation="fullUser">
The difference between using unspecified
and fullUser
is subtle but important. If you don't declare a screenOrientation
value, the system chooses the orientation, and the policy the system uses to define the orientation might differ from device to device. On the other hand, specifying fullUser
matches more closely the behavior the user defined for the device: if the user has locked sensor-based rotation, the app follows the user preference; otherwise, the system allows any of the four possible screen orientations (portrait, landscape, reverse portrait, or reverse landscape). See android:screenOrientation
.
2. Determine screen size
With the manifest set to support all user-permitted orientations, you can specify app orientation programmatically based on screen size.
Add the Jetpack WindowManager libraries to the module's build.gradle
or build.gradle.kts
file:
Kotlin
implementation("androidx.window:window:version
") implementation("androidx.window:window-core:version
")
Groovy
implementation 'androidx.window:window:version
' implementation 'androidx.window:window-core:version
'
Use the Jetpack WindowManager WindowMetricsCalculator#computeMaximumWindowMetrics()
method to obtain the device screen size as a WindowMetrics
object. The window metrics can be compared to window size classes to decide when to restrict orientation.
Windows size classes provide the breakpoints between small and large screens.
Use the WindowWidthSizeClass#COMPACT
and WindowHeightSizeClass#COMPACT
breakpoints to determine the screen size:
Kotlin
/** Determines whether the device has a compact screen. **/ fun compactScreen() : Boolean { val metrics = WindowMetricsCalculator.getOrCreate().computeMaximumWindowMetrics(this) val width = metrics.bounds.width() val height = metrics.bounds.height() val density = resources.displayMetrics.density val windowSizeClass = WindowSizeClass.compute(width/density, height/density) return windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT || windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT }
Java
/** Determines whether the device has a compact screen. **/ private boolean compactScreen() { WindowMetrics metrics = WindowMetricsCalculator.getOrCreate().computeMaximumWindowMetrics(this); int width = metrics.getBounds().width(); int height = metrics.getBounds().height(); float density = getResources().getDisplayMetrics().density; WindowSizeClass windowSizeClass = WindowSizeClass.compute(width/density, height/density); return windowSizeClass.getWindowWidthSizeClass() == WindowWidthSizeClass.COMPACT || windowSizeClass.getWindowHeightSizeClass() == WindowHeightSizeClass.COMPACT; }
- Note:
- The above examples are implemented as methods of an activity; and so, the activity is dereferenced as
this
in the argument ofcomputeMaximumWindowMetrics()
. - The
computeMaximumWindowMetrics()
method is used instead ofcomputeCurrentWindowMetrics()
because the app can be launched in multi-window mode, which ignores the screen orientation setting. There's no point in determining the app window size and overriding the orientation setting unless the app window is the entire device screen.
See WindowManager for instructions about declaring dependencies to make the computeMaximumWindowMetrics()
method available in your app.
3. Override app manifest setting
When you've determined that the device has compact screen size, you can call Activity#setRequestedOrientation()
to override the manifest's screenOrientation
setting:
Kotlin
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) requestedOrientation = if (compactScreen()) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT else ActivityInfo.SCREEN_ORIENTATION_FULL_USER ... // Replace with a known container that you can safely add a // view to where the view won't affect the layout and the view // won't be replaced. val container: ViewGroup = binding.container // Add a utility view to the container to hook into // View.onConfigurationChanged. This is required for all // activities, even those that don't handle configuration // changes. You can't use Activity.onConfigurationChanged, // since there are situations where that won't be called when // the configuration changes. View.onConfigurationChanged is // called in those scenarios. container.addView(object : View(this) { override fun onConfigurationChanged(newConfig: Configuration?) { super.onConfigurationChanged(newConfig) requestedOrientation = if (compactScreen()) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT else ActivityInfo.SCREEN_ORIENTATION_FULL_USER } }) }
Java
@Override protected void onCreate(Bundle savedInstance) { super.onCreate(savedInstanceState); if (compactScreen()) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } else { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_USER); } ... // Replace with a known container that you can safely add a // view to where the view won't affect the layout and the view // won't be replaced. ViewGroup container = binding.container; // Add a utility view to the container to hook into // View.onConfigurationChanged. This is required for all // activities, even those that don't handle configuration // changes. You can't use Activity.onConfigurationChanged, // since there are situations where that won't be called when // the configuration changes. View.onConfigurationChanged is // called in those scenarios. container.addView(new View(this) { @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); if (compactScreen()) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } else { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_USER); } } }); }
By adding the logic to the onCreate()
and View.onConfigurationChanged()
methods, you're able to obtain the maximum window metrics and override the orientation setting whenever the activity is resized or moved between displays, such as after a device rotation or when a foldable device is folded or unfolded.
For more information about when configuration changes occur and when they cause activity recreation, refer to Handle configuration changes
Results
Your app should now remain in portrait orientation on small screens regardless of device rotation. On large screens, the app should support landscape and portrait orientations.
Additional resources
For help with upgrading your app to support all device configurations all the time, see the following:
Media playback pause and resume with external keyboard Spacebar
Large screen optimization includes the ability to handle external keyboard inputs, like reacting to the Spacebar being pressed to pause or resume playback of videos and other media. This is particularly useful for tablets, which often connect to external keyboards, and Chromebooks, which usually come with external keyboards but can be used in tablet mode.
When media is the only element of the window (like full-screen video playback), respond to keypress events at the activity level or, in Jetpack Compose, at the screen level.
Best practices
Whenever your app plays a media file, users should be able to pause and resume playback by pressing the Spacebar on a physical keyboard.
Ingredients
KEYCODE_SPACE
: Key code constant for the Spacebar.
Compose
onPreviewKeyEvent
:Modifier
that enables a component to intercept hardware key events when it (or one of its children) is focused.onKeyEvent
: Similar toonPreviewKeyEvent
, thisModifier
enables a component to intercept hardware key events when it (or one of its children) is focused.
Views
onKeyUp()
: Called when a key is released and not handled by a view within an activity.
Steps
Summary
View-based apps and apps based on Jetpack Compose respond to keyboard key presses in similar ways: the app must listen for keypress events, filter the events, and respond to selected keypresses, such as a Spacebar keypress.
1. Listen for keyboard events
Views
In an activity in your app, override the onKeyUp()
method:
Kotlin
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { ... }
Java
@Override public boolean onKeyUp(int keyCode, KeyEvent event) { ... }
The method is invoked every time a pressed key is released, so it fires exactly once for every keystroke.
Compose
With Jetpack Compose, you can leverage either the onPreviewKeyEvent
or the onKeyEvent
modifier within the screen that manages the keystroke:
Column(modifier = Modifier.onPreviewKeyEvent { event ->
if (event.type == KeyEventType.KeyUp) {
...
}
...
})
or
Column(modifier = Modifier.onKeyEvent { event ->
if (event.type == KeyEventType.KeyUp) {
...
}
...
})
2. Filter Spacebar presses
Inside the onKeyUp()
method or the Compose onPreviewKeyEvent
and onKeyEvent
modifier methods, filter for KeyEvent.KEYCODE_SPACE
to send the correct event to your media component:
Views
Kotlin
if (keyCode == KeyEvent.KEYCODE_SPACE) { togglePlayback() return true } return false
Java
if (keyCode == KeyEvent.KEYCODE_SPACE) { togglePlayback(); return true; } return false;
Compose
Column(modifier = Modifier.onPreviewKeyEvent { event ->
if (event.type == KeyEventType.KeyUp && event.key == Key.Spacebar) {
...
}
...
})
or
Column(modifier = Modifier.onKeyEvent { event ->
if (event.type == KeyEventType.KeyUp && event.key == Key.Spacebar) {
...
}
...
})
Results
Your app can now respond to Spacebar key presses to pause and resume a video or other media.
Additional resources
To learn more about keyboard events and how to manage them, see Handle keyboard input.
Stylus palm rejection
A stylus can be an exceptionally productive and creative tool on large screens. But when users draw, write, or interact with an app using a stylus, they sometimes touch the screen with the palm of their hands. The touch event can be reported to your app before the system recognizes and dismisses the event as an accidental palm touch.
Best practices
Your app must identify extraneous touch events and ignore them. Android cancels a palm touch by dispatching a MotionEvent
object. Check the object for ACTION_CANCEL
or ACTION_POINTER_UP
and FLAG_CANCELED
to determine whether to reject the gesture caused by the palm touch.
Ingredients
MotionEvent
: Represents touch and movement events. Contains the information necessary to determine whether an event should be disregarded.OnTouchListener#onTouch()
: ReceivesMotionEvent
objects.MotionEvent#getActionMasked()
: Returns the action associated with a motion event.ACTION_CANCEL
:MotionEvent
constant that indicates a gesture should be undone.ACTION_POINTER_UP
:MotionEvent
constant that indicates a pointer other than the first pointer has gone up (that is, has relinquished contact with the device screen).FLAG_CANCELED
:MotionEvent
constant that indicates that the pointer going up caused an unintentional touch event. Added toACTION_POINTER_UP
andACTION_CANCEL
events on Android 13 (API level 33) and higher.
Steps
Summary
Examine MotionEvent
objects dispatched to your app. Use the MotionEvent
APIs to determine event characteristics:
- Single-pointer events — Check for
ACTION_CANCEL
. On Android 13 and higher, also check forFLAG_CANCELED
. - Multi-pointer events — On Android 13 and higher, check for
ACTION_POINTER_UP
andFLAG_CANCELED
.
Respond to ACTION_CANCEL
and ACTION_POINTER_UP
/FLAG_CANCELED
events.
1. Acquire motion event objects
Add an OnTouchListener
to your app:
Kotlin
val myView = findViewById<View>(R.id.myView).apply { setOnTouchListener { view, event -> // Process motion event. } }
Java
View myView = findViewById(R.id.myView); myView.setOnTouchListener( (view, event) -> { // Process motion event. });
2. Determine the event action and flags
Check for ACTION_CANCEL
, which indicates a single-pointer event on all API levels. On Android 13 and higher, check ACTION_POINTER_UP
for FLAG_CANCELED.
Kotlin
val myView = findViewById<View>(R.id.myView).apply { setOnTouchListener { view, event -> when (event.actionMasked) { MotionEvent.ACTION_CANCEL -> { //Process canceled single-pointer motion event for all SDK versions. } MotionEvent.ACTION_POINTER_UP -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && (event.flags and MotionEvent.FLAG_CANCELED) == MotionEvent.FLAG_CANCELED) { //Process canceled multi-pointer motion event for Android 13 and higher. } } } true } }
Java
View myView = findViewById(R.id.myView); myView.setOnTouchListener( (view, event) -> { switch (event.getActionMasked()) { case MotionEvent.ACTION_CANCEL: // Process canceled single-pointer motion event for all SDK versions. case MotionEvent.ACTION_UP: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && (event.getFlags() & MotionEvent.FLAG_CANCELED) == MotionEvent.FLAG_CANCELED) { //Process canceled multi-pointer motion event for Android 13 and higher. } } return true; });
3. Undo the gesture
Once you've identified a palm touch, you can undo the onscreen effects of the gesture.
Your app must keep a history of user actions so that unintended inputs such as palm touches can be undone. See Implement a basic drawing app in the Enhance stylus support in an Android app codelab for an example.
Results
Your app can now identify and reject palm touches for multi-pointer events on Android 13 and higher API levels and for single-pointer events on all API levels.
Additional resources
For more information, see the following:
- Android 13 features and APIs — Improved palm rejection
- Developer guides
- Codelab — Enhance stylus support in an Android app
WebView state management
WebView
is a commonly used component that offers an advanced system for state management. A WebView
must maintain its state and scroll position across configuration changes. A WebView
can lose scroll position when the user rotates the device or unfolds a foldable phone, which forces the user to scroll again from the top of the WebView
to the previous scroll position.
Best practices
Minimize the number of times a WebView
is recreated. WebView
is good at managing its state, and you can leverage this quality by managing as many configuration changes as possible. Your app must handle configuration changes because Activity
recreation (the system's way of handling configuration changes) recreates the WebView
as well, which causes the WebView
to lose its state.
Ingredients
android:configChanges
: Attribute of the manifest<activity>
element. Lists the configuration changes handled by the activity.View#invalidate()
: Method that causes a view to be redrawn. Inherited byWebView
.
Steps
Summary
To save the WebView
state, avoid Activity
recreation as much as possible, and then let the WebView
invalidate so that it can resize while retaining its state.
1. Add configuration changes to your app's AndroidManifest.xml
file
Avoid activity recreation by specifying the configuration changes handled by your app (rather than by the system):
<activity
android:name=".MyActivity"
android:configChanges="screenLayout|orientation|screenSize
|keyboard|keyboardHidden|smallestScreenSize" />
2. Invalidate WebView
whenever your app receives a configuration change
Kotlin
override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) webView.invalidate() }
Java
@Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); webview.invalidate(); }
This step applies only to the view system, as Jetpack Compose does not need to invalidate anything to resize Composable
elements correctly. However, Compose recreates a WebView
often if not managed correctly. Use the Accompanist WebView wrapper to save and restore WebView
state in your Compose apps.
Results
Your app's WebView
components now retain their state and scroll position across multiple configuration changes, from resizing to orientation change to folding and unfolding.
Additional resources
To learn more about configuration changes and how to manage them, see Handle configuration changes.
RecyclerView state management
RecyclerView
can display large amounts of data using minimal graphical resources. As a RecyclerView
scrolls through its list of items, the RecyclerView
reuses the View
instances of items that have scrolled off screen to create new items as they scroll on screen. But configuration changes, such as device rotation, can reset the state of a RecyclerView
, forcing users to again scroll to their previous position in the list of RecyclerView
items.
Best practices
RecyclerView
should maintain its state—in particular, scroll position—and the state of its list elements during all configuration changes.
Ingredients
RecyclerView.Adapter#setStateRestorationPolicy()
: Specifies how aRecyclerView.Adapter
restores its state after a configuration change.ViewModel
: Holds state for an activity or fragment.
Steps
Summary
Set the state restoration policy of the RecyclerView.Adapter
to save the RecyclerView
scroll position. Save the state of RecyclerView
list items. Add the state of the list items to the RecyclerView
adapter, and restore the state of list items when they're bound to a ViewHolder
.
1. Enable Adapter
state restoration policy
Enable the state restoration policy of the RecyclerView
adapter so that the scrolling position of the RecyclerView
is maintained across configuration changes. Add the policy specification to the adapter constructor:
Kotlin
class MyAdapter() : RecyclerView.Adapter<RecyclerView.ViewHolder>() { init { stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY } ... }
Java
class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { public Adapter() { setStateRestorationPolicy(StateRestorationPolicy.PREVENT_WHEN_EMPTY); } ... }
2. Save the state of stateful list items
Save the state of complex RecyclerView
list items, such as items that contain EditText
elements. For example, to save the state of an EditText
, add a callback similar to an onClick
handler to capture text changes. Within the callback, define what data to save:
Kotlin
input.addTextChangedListener( afterTextChanged = { text -> text?.let { // Save state here. } } )
Java
input.addTextChangedListener(new TextWatcher() { ... @Override public void afterTextChanged(Editable s) { // Save state here. } });
Declare the callback in your Activity
or Fragment
. Use a ViewModel
to store the state.
3. Add list item state to the Adapter
Add the state of list items to your RecyclerView.Adapter
. Pass the item state to the adapter constructor when your host Activity
or Fragment
is created:
Kotlin
val adapter = MyAdapter(items, viewModel.retrieveState())
Java
MyAdapter adapter = new MyAdapter(items, viewModel.retrieveState());
4. Recover list item state in the adapter's ViewHolder
In the RecyclerView.Adapter
, when you bind a ViewHolder
to an item, restore the item's state:
Kotlin
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { ... val item = items[position] val state = states.firstOrNull { it.item == item } if (state != null) { holder.restore(state) } }
Java
@Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { ... Item item = items[position]; Arrays.stream(states).filter(state -> state.item == item) .findFirst() .ifPresent(state -> holder.restore(state)); }
Results
Your RecyclerView
is now able to restore its scroll position and the state of every item in the RecyclerView
list.
Additional resources
Detachable keyboard management
Support for detachable keyboards helps maximize user productivity on large
screen devices. Android triggers a configuration change every time a keyboard is
attached to or detached from a device, which can cause a loss of UI state. Your
app can either save and restore its state, letting the system handle
activity recreation, or restrict activity recreation for keyboard configuration changes.
In all cases all data related to the keyboard is stored in a
Configuration
object. The keyboard
and
keyboardHidden
fields of the configuration object contain information about the type
of keyboard and its availability.
Best practices
Apps optimized for large screens support every type of input device, from software and hardware keyboards to stylus, mouse, trackpad, and other peripheral devices.
Support for external keyboards involves configuration changes, which you can manage in either of two ways:
- Let the system recreate the currently running activity, and you take care of managing the state of your app.
- Manage the configuration change yourself (the activity won't be recreated):
- Declare all keyboard-related configuration values
- Create a configuration change handler
Productivity apps, which often require fine control of the UI for text entry and other input, can benefit from the do-it-yourself approach to handling configuration changes.
In special cases, you might want to change your app layout when a hardware keyboard is attached or detached, for example, to make more space for tools or editing windows.
Since the only reliable way to listen for configuration changes is to override
the onConfigurationChanged()
method of a view, you can add a new View
instance to your app activity and respond in the view's onConfigurationChanged()
handler to configuration changes caused by the keyboard being attached or
detached.
Ingredients
android:configChanges
: Attribute of the app manifest's<activity>
element. Informs the system about configuration changes the app manages.View#onConfigurationChanged()
: Method that reacts to propagation of a new app configuration.
Steps
Summary
Declare the configChanges
attribute and add keyboard-related values. Add a
View
to the activity's view hierarchy and listen for configuration changes.
1. Declare configChanges
attribute
Update the <activity>
element in the app manifest by adding the keyboard|keyboardHidden
values to the list of already managed configuration changes:
<activity
…
android:configChanges="...|keyboard|keyboardHidden">
2. Add an empty view to the view hierarchy
Declare a new view and add your handler code inside the view's onConfigurationChanged()
method:
Kotlin
val v = object : View(this) { override fun onConfigurationChanged(newConfig: Configuration?) { super.onConfigurationChanged(newConfig) // Handler code here. } }
Java
View v = new View(this) { @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); // Handler code here. } };
Results
Your app will now respond to an external keyboard being attached or detached without recreating the currently running activity.
Additional resources
To learn how to save your app's UI state during configuration changes like keyboard attachment or detachment, see Save UI states.
Recommended for you
- Note: link text is displayed when JavaScript is off
- Handle configuration changes