This library provides a set of APIs that allow you to:
- Define hierarchical default focus for groups of focusable composables
- Hierarchically preserve focus history for groups of focusable composables
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:
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
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.
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.
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 object
s. 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 childFocusContainer
objects. Typically you should useDefaultFocusSaverImpl
and save it in your ViewModel. You may also wish to preserve it in saved instance state, and it isParcelable
to allow for this. - A
GetDefaultFocusId
function - Each time the root gains focus this function will run, and receive a list of childFocusIds
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
- AllFocusContainers
must define aFocusId
. The ID passed as a parameter takes precedence over any passed in theModifier
. - 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”.