I’m trying to implement a `Modifier` that, when th...
# compose
a
I’m trying to implement a
Modifier
that, when the element it modifies is “selected” (in a sense I define), it automatically makes it focused.
Copy code
fun Modifier.focusedWhenSelected(): Modifier = this.then(FocusedWhenSelected)

object FocusedWhenSelected: Modifier.Element{

    fun implementingModifier(modifier: Modifier, isSelected: Boolean): Modifier{
        val hasFocusedWhenSelected = modifier.any { it == FocusedWhenSelected }
        if (!hasFocusedWhenSelected)
            return Modifier

        return Modifier.composed {
            val focusRequester = remember { FocusRequester() }
            if (isSelected) {
                LaunchedEffect(Unit) {
                    focusRequester.requestFocus()
                }
            }

            Modifier
                .focusRequester(focusRequester)
                .focusable()
        }
    }
}
and then, in the code that actually defines
isSelected
, I do:
Copy code
Box(
    modifier = FocusedWhenSelected.implementingModifier(modifier, isSelected)
){
...
}
Does this look right? I’m somewhat worried that I’m creating a new
Modifier.composed
every time.
z
Don’t pass
Unit
to launched effect, pass its dependencies. In this case that’s
focusRequester
.
But this looks smelly. Manually examining the modifier chain at this point is not good because composed modifiers will not have been materialized yet. But the only reason you’re doing that is to figure out whether to apply this modifier based on the presence of another modifier – why do you need 2 modifiers? Just apply this one when you need it.
I’m somewhat worried that I’m creating a new
Modifier.composed
every time.
Well that’s how
composed
works. I think there are other issues with this approach that should be addressed before worrying about that.
I think the biggest issue is that you’ve now got two sources of truth for focus/selection, and you have to synchronize them both ways – when something is selected, you need to focus it, and when it’s focused, you presumably need to mark it as selected. Why not just use the focus state as the source of truth for selection as well?
a
No, no, “isSelected” is the source of truth.
I don’t think in this case it’s important to pass
focusRequester
as the key to
LaunchedEffect
- if
focusRequester
changes, it’s a new composition anyway, and the effect will be launched again. Am I wrong? Not sure what you mean about two modifiers - there is only one modifier (
FocusedWhenSelected
). I’m examining the chain to see whether it’s present, and if it is, I “apply” it. It just happens that applying it involves using some other modifiers.
I can’t separate the
focusRequester().focusable()
modifier from the
LaunchedEffect
requesting focus because it needs the
focusRequester
FocusedWhenSelected
is just a marker - it doesn’t “do” anything by itself, it just tells the code that actually knows about whether the element is “selected” that it should give it focus.
implementingModifier
is inside
FocusedWhenSelected
just for scoping. It could’ve been a top-level function just as well.
z
Was on vacation, now I’m back!
No, no, “isSelected” is the source of truth.
It’s one of two. The focus system maintains its own private state about what is currently focused, which is the second source of truth that you can’t avoid if you want to interact with focus at all. The reason Compose doesn’t let you hoist focus state yourself is probably because it’s pretty complex and can come from the system, although I’m not 100% sure it’s impossible. That said, since you can’t get rid of the focus state as a source of truth, the only source you can eliminate is your own.
I don’t think in this case it’s important to pass
focusRequester
as the key to
LaunchedEffect
- if
focusRequester
changes, it’s a new composition anyway, and the effect will be launched again. Am I wrong?
That’s true, for now, but code has a way of growing and not following best practices like this can introduce little bugs later.
LaunchedEffect
requires a parameter anyway – if you have to pass something in, why not pass in the thing that it’s designed to take?
Not sure what you mean about two modifiers - there is only one modifier (
FocusedWhenSelected
).
There’s also the composed modifier you’re returning from
implementingModifier
. My question was why do you need that indirection? Why can’t you just have your consumers apply
implementingModifier
when they want something to be focused-when-selected? This:
Copy code
Box(
    modifier = FocusedWhenSelected.implementingModifier(modifier, isSelected)
)
could just be this (although probably you’d want a different name):
Copy code
Box(
    modifier = Modifier.implementingModifier(modifier, isSelected)
)
That said, if the answer is because the thing making the decision about whether to apply
FocusedWhenSelected
is in an entirely different part of the code than the one applying
implementingModifier
, then you’d be better off using ModifierLocals for this instead of trying to look for the presence of a specific modifier in the chain yourself.
a
To the last part - the answer is indeed that the code that specifies the
focusedWhenSelected
modifier is not the same code that decides whether the element is “selected”. I’ll look into ModifierLocals. It sounds promising. To the first part - I probably haven’t explained myself well.
isSelected
is completely my own thing. It does not at all have to be related to focus. The modifier I’m trying to implement here is what creates this relation. The relation being that when an element is deemed “selected”, it should also automatically get focus.
@Zach Klippenstein (he/him) [MOD] Thanks!
ModifierLocal
worked wonderfully:
Copy code
/**
 * When the given [ModifierLocal]'s value is `true`, requests focus for the node to which this modifier is applied.
 */
@Composable
fun Modifier.requestFocusWhen(modifierLocal: ModifierLocal<Boolean>, focusRequester: FocusRequester) = composed {
    var requestFocus by remember { mutableStateOf(false) }
    if (requestFocus){
        LaunchedEffect(Unit) {
            focusRequester.requestFocus()
        }
    }

    this
        .focusable()
        .modifierLocalConsumer {
            requestFocus = modifierLocal.current
        }
}


/**
 * Specifies the "selected" state of the node.
 */
private val SelectedLocalModifier: ProvidableModifierLocal<Boolean> = modifierLocalOf { false }


/**
 * Sets the "selected" state of the node to which this modifier is applied.
 * Together with [requestFocusWhenSelected], this can be used to automatically transfer focus to the currently selected item.
 */
fun Modifier.markSelected(isSelected: Boolean) = this.modifierLocalProvider(SelectedLocalModifier){ isSelected }


/**
 * Requests focus using the given [FocusRequester] when the node, or one of its parents is marked as selected via
 * [markSelected].
 */
@Composable
fun Modifier.requestFocusWhenSelected(focusRequester: FocusRequester) = requestFocusWhen(SelectedLocalModifier, focusRequester)