Skip to content

PhiloInc/compose-focus-container

Repository files navigation

FocusContainer: a simpler way to manage focus in Compose

This library provides a set of APIs that allow you to:

  1. Define hierarchical default focus for groups of focusable composables
  2. Hierarchically preserve focus history for groups of focusable composables

Status: Alpha

Background

At Philo, we had developed a fairly fully featured focus framework for managing focus using XML based layouts and when Compose came along we found that the focus APIs did not provide us with flexibility, and in some cases, made it too easy to run into runtime crashes.

There are a few basic concepts we need when it comes to managing focus:

Default Focus

Groups of focusable items should be able to manage their own default focus. For example, you you might have a group of buttons next to a column of tiles. The visibility of the buttons can change dynamically, as can the visibility of the items in the list. When you focus from one side to the other, you should be able to define which element gets default focus. Additionally, in some cases, you may want a hierarchy of default focus:

if the play button is there, focus that
if the channel button is there, focus that
etc

Focus Restoration

Sometimes you may want to restore focus back to where it was either when the user re-enters a screen or when they re-enter a group of focusable items. In either case, it should be easy to distinguish between default focus, and focus restoration. You should not need to write code like:

if (userHadPreviouslyFocusedThisThing && thisThingIsStillVisible) {
  previousFocusRequester.requestFocus()
} else if (playButtonButtonVisible) {
  playButtonFocusRequest.requestFocus()
} ...

It should be easy to persist however needed, ideally in the view model - the focus state should persist even if the screen goes away and returns.

It should be possible to clear focus history for a group of focusable elements, and it should be possible to completely ignore focus history for focusable elements.

Back Handling

In older API versions, when an item has focused the backhandler fails to function correctly. See: https://issuetracker.google.com/issues/380990242

To account for this, we force users to set a back handler using our "safe" code on an composable that could contain focusable elements. We also allow the back handlers to be defined at any level of the hierarchy with the lowest level taking precedence.

Solution

To facilitate all of these features, we introduce the concept of a FocusId. In order to receive default focus, be preserved in focus history, and have back button handling, a composable must be assigned a FocusId via Modifer.focusId(). The ID itself is arbitrary, and there are convenience functions to create IDs from a String or an Int. You may also use arbitrary objects. Once a composable has a FocusId you may retrieve its FocusRequester at any time via FocusId.asFocusRequester, which is itself a composable.

To use the framework, you must wrap all of your composables in a FocusContainerRoot. This composable is simply a Box with a few parameters that you must pass:

  • A FocusSaver - This object will preserve focus history for the root, and all child FocusContainer objects. Typically you should use DefaultFocusSaverImpl and save it in your ViewModel. You may also wish to preserve it in saved instance state, and it is Parcelable to allow for this.
  • A GetDefaultFocusId function - Each time the root gains focus this function will run, and receive a list of child FocusIds that are available to gain focus.
  • initallyFocused - If true, this container will request focus as soon as it composes for the
    first time.
  • backHandler - This becomes the default back handler for the tree. Children may override this back handler. When a focused composable receives a back press, the framework will traverse up the tree until it finds a container with a back handler defined.

You may put FocusContainers as many levels deep as you wish within the FocusContainerRoot. FocusContainers are also simple Boxes . They have two additional parameters:

  • A focusId - All FocusContainers must define a FocusId. The ID passed as a parameter takes precedence over any passed in the Modifier.
  • A boolean clearSaverOnExit - If true, focus history will not be preserved when
  • the user takes action to exit the container. NOTE! if the user navigates forward from a
  • container, that does not count as an “exit”.

About

Focus management for Compose

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages