One Off the Slack: Issues with Dependent Modifiers

Chris Stratton asked:

focusable(...) does not seem to be working as advertised? I’m trying to make a Box (or Surface - I’m not fussy :grin:) be focusable so have modified it to:

Documentation suggests this is all we need for a custom composable to be focused, and yet, when I call requestFocus() on my Box’s focus requester, the previously focused component (a regular TextField) does not appear to ‘let go’ of it’s focus, also TalkBack does not shift it’s highlight to the Box component. What else do I need to do to effect a full transfer of focus over to my custom component?

Ryan Nett responded with the solution:

Make sure you’re putting the focusOrder first, that’s gotten me a couple of times.

In other words, Chris had something akin to:

Box(modifier = Modifier.focusable(true).focusOrder(myFocusRequester))

…and the solution is to invert the order of those modifiers:

Box(modifier = Modifier.focusOrder(myFocusRequester).focusable(true))

Those two modifiers do not need to be used together, but when they are, there is a specific order in which they need to be used. Modifiers do not necessarily know about other modifiers, and so it may be difficult for focusOrder() to check to see that focusable() was called previously.

So, the API is technically correct, yet it represents a footgun.

Chris was happy to find the solution but unhappy about how it all went down:

This feels like such an obvious gotcha for devs trying to use these two modifiers in combination; it really would be quite easy to include in the API comments. Not proud to admit this probably cost me a day 😞

As you design your own modifiers for your own composables, be careful about creating modifiers that have a strict dependency on their order of application.

One solution is to maintain the independent modifiers but have parameter options that allow their effects to be combined in a single modifier. For example, in this case, focusable() could take an optional FocusRequester parameter, eliminating the need for a separate focusOrder() call, and allowing focusable() to ensure that everything is done properly.

If that is impractical, try to find ways to at least help developers identify footgun scenarios:

Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!