Large screen cookbook

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.

Five-star rating Meets the criteria for Tier 1, Large screen differentiated
Four-star rating Meets the criteria for Tier 2, Large screen optimized
Three-star rating Meets the criteria for Tier 3, Large screen ready
Two-star rating Provides some large screen capabilities, but falls short of the large screen app quality guidelines
One-star rating Meets the needs of a specific use case, but doesn't properly support large screens

Chromebook camera support

Three-star rating

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 app
  • required 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

Two-star rating

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 of computeMaximumWindowMetrics().
  • The computeMaximumWindowMetrics() method is used instead of computeCurrentWindowMetrics() 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

Four-star rating

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

Compose

  • onPreviewKeyEvent: Modifier that enables a component to intercept hardware key events when it (or one of its children) is focused.
  • onKeyEvent: Similar to onPreviewKeyEvent, this Modifier 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

Five-star rating

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(): Receives MotionEvent 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 to ACTION_POINTER_UP and ACTION_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 for FLAG_CANCELED.
  • Multi-pointer events — On Android 13 and higher, check for ACTION_POINTER_UP and FLAG_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:

WebView state management

Three-star rating

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 by WebView.

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

Three-star rating

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

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

Three-star rating

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:

  1. Let the system recreate the currently running activity, and you take care of managing the state of your app.
  2. 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.