Do you think I can do focus requester like this? I...
# compose
p
Do you think I can do focus requester like this? I guess it will re-request focus every time
Test()
re-composes? 🙂
Copy code
fun Modifier.requestFocusSingleTime(): Modifier = composed {
    val requester = remember { FocusRequester() }
    LaunchedEffect(Unit) {
        requester.requestFocus()
    }
    focusRequester(requester)
}

@Composable
fun Test() {
    TextField(modifier = Modifier.requestFocusSingleTime())
}
🚫 1
seems to work ok. Any thoughts about this?
v
I have a feeling that you might get an exception randomly doing this. At least on some versions
LaunchedEffect
might randomly execute before the focus requester is registered in the tree.
This would be the exception:
Copy code
java.lang.IllegalStateException:
FocusRequester is not initialized. Here are some possible fixes:

    1. Remember the FocusRequester: val focusRequester = remember { FocusRequester() }
    2. Did you forget to add a Modifier.focusRequester() ?
    3. Are you attempting to request focus during composition? Focus requests should be made in
    response to some event. Eg Modifier.clickable { focusRequester.requestFocus() }
SideEffect
runs after the composition so you won't get any nasty race conditions like that, but then you will have to keep track of whether you already ran it or not.
You could also probably use
Copy code
this.coroutineContext.job.invokeOnCompletion {}
with
LaunchedEffect
Not sure what would be the idiomatic way to do this 🤔
But I'm quite sure you will get a race condition throwing exceptions with your initial solution 😅
p
I guess I can maybe still do the following, to address this issue?
Copy code
focusRequester(requester).also {
    LaunchedEffect(){...}
}
v
No, that doesn't affect the issue
z
It would be better to not use
composed
, which is terrible for performance. You can make a
Modifier.Node
implement
FocusRequesterModifierNode
, then either call
requestFocus
in
onAttach
or, if that’s too early in the modifier lifecycle (I’m not sure if it is), a coroutine launched from it.
thank you color 2
v
That's an excellent point Zach. I've been meaning to refactor some of our custom
composed
modifiers for some time to use the Modifier.Node api... Iirc there's no migration guide yet?
z
Leland wrote a nice one but it’s internal only afaik. @Leland Richardson [G] any plans to publish that?
👀 1
v
Oh, cool! Let's hope that could be published at some point 😄 I've been putting off the refactoring a bit too long now
p
I've been digging around samples and documentation, and I've come up with following code. Calling request focus in
onAttach()
looks like it's too early in the lifecycle, since it throws "that exception" above. I've also tried
sideEffect()
but no luck. @Zach Klippenstein (he/him) [MOD] can you please give me an example, how would you launch a coroutine 🙏 Also, I'm wondering, if there's any way to avoid having
FocusRequester
as a dependency somehow.
Copy code
fun Modifier.requestFocusOnce(requester: FocusRequester): Modifier =
    this then RequestFocusElement(requester)

private class RequestFocusNode(
    var focusRequester: FocusRequester
) : Modifier.Node() {

    @OptIn(ExperimentalComposeUiApi::class)
    override fun onAttach() {
        super.onAttach()
        sideEffect {
            focusRequester.requestFocus()
        }
    }
}

private data class RequestFocusElement(
    private val focusRequester: FocusRequester
) : ModifierNodeElement<RequestFocusNode>() {
    override fun create(): RequestFocusNode =
        RequestFocusNode(focusRequester)

    override fun update(node: RequestFocusNode) {
        node.focusRequester = focusRequester
    }
}
a
You can just call
coroutineScope.launch {}
in
onAttach
. Note that
coroutineScope
is available since compose 1.5.0.
2
p
I've tried doing that,
FocusRequester is not initialized
was still thrown
e
I dont see where you are attatching that focus requester to the focus system 🤔 (viz
.focusRequester(focusRequester)
)
But that doesnt attach it to the focus system. That only sets up your custom logic. I believe thats why Zach mentioned implementing
FocusRequesterModifierNode
, that is a known node by the system
p
ah, right, poop
e
LaunchedEffect
might randomly execute before the focus requester is registered in the tree.
If this happens this is a bug from Compose runtime. LaunchedEffect is still an effect, it will not execute concurrently with composition (nor before).
a
This seems to work:
Copy code
fun Modifier.requestFocusOnce(): Modifier =
    this then RequestFocusElement

private data object RequestFocusElement : ModifierNodeElement<RequestFocusNode>() {
    override fun create(): RequestFocusNode = RequestFocusNode()
    override fun update(node: RequestFocusNode) = Unit
}

private class RequestFocusNode : FocusRequesterModifierNode, Modifier.Node() {
    override fun onAttach() {
        requestFocus()
    }
}
👍 1
👍🏼 1
👍🏻 1
p
Nice solution! I still needed to launch a coroutine, otherwise it seems to crash
Copy code
override fun onAttach() {
    coroutineScope.launch {
        requestFocus()
    }
}
Copy code
java.lang.IllegalStateException: Check failed.
	at androidx.compose.ui.focus.FocusTargetModifierNode.fetchFocusProperties$ui_release(FocusTargetModifierNode.kt:210)
	at androidx.compose.ui.focus.FocusRequesterModifierNodeKt.requestFocus(FocusRequesterModifierNode.kt:41)
	at androidx.compose.ui.Modifier$Node.attach$ui_release(Modifier.kt:248)
	at androidx.compose.ui.node.NodeChain.attach(NodeChain.kt:271)
	at androidx.compose.ui.node.LayoutNode.attach$ui_release(LayoutNode.kt:435)
	at androidx.compose.ui.node.LayoutNode.attach$ui_release(LayoutNode.kt:437)
	at androidx.compose.ui.node.LayoutNode.attach$ui_release(LayoutNode.kt:437)
	at androidx.compose.ui.node.LayoutNode.attach$ui_release(LayoutNode.kt:437)
	at androidx.compose.ui.node.LayoutNode.attach$ui_release(LayoutNode.kt:437)
	at androidx.compose.ui.node.LayoutNode.insertAt$ui_release(LayoutNode.kt:299)
	at androidx.compose.ui.node.UiApplier.insertBottomUp(UiApplier.android.kt:31)
	at androidx.compose.ui.node.UiApplier.insertBottomUp(UiApplier.android.kt:21)
	at androidx.compose.runtime.ComposerImpl$createNode$3.invoke(Composer.kt:1622)
	at androidx.compose.runtime.ComposerImpl$createNode$3.invoke(Composer.kt:1617)
v
@efemoney fwiw, the docs only state
SideEffect
runs after the composition:
Schedule
effect
to run when the current composition completes successfully and applies changes.
https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#SideEffect(kotlin.Function0)
s
Isn’t it that
SideEffect
and
DisposableEffect
run directly after the first successful composition, while
LaunchedEffect
runs after the first composition but also due to coroutine scheduling 1 frame later too? Which is a reason why for some things if you want them to be keyed or run just once (so gotta go with LaunchedEffect or DisposableEffect) you go with
DisposableEffect(Unit) { doThing; onDispose{} }
so that you get the faster action and an empty onDispose block? Like here for example
s
Ah that’s exactly the message I had in mind but couldn’t find, thanks for referencing it here Albert!
e
@vide Agreed the kdocs dont call it out explicitly but other material always has. But to confirm from the horses mouth this is where they are both “fired” https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]osition.kt;drc=2aff88f0043752768551a77b7de84644a18e6bc5;l=976 side effects & remember observers (which launched & disposable effects are based on)
v
Hmm. Has the behaviour changed then? Because I definitely remember trying to use FocusRequesters with LaunchedEffect running before they are registered 🤔
What phase is the FocusRequester registered in? And has it changed between the
composed
and
Modifier.Node
versions?
e
That exception is thrown when that focus requester is not register with any node during composition.
There should be no difference between composed or Node implementation (other than maybe performance) because composed does “JIT composition” but will still produce a modifier that will be attached to a layout node. The node level is the level at which the focus stuff works.
LaunchedEffect running before they are registered
If you still have a reproducer I would suggest submitting an issue 🙏🏾
v
@efemoney so just to clarify: you think this shouldn't cause any problems?
Copy code
val f = remember { FocusRequester() }
Box(Modifier.focusRequester(f).focusTarget())
LaunchedEffect(Unit) { f.requestFocus() }
e
Yes, most definitely (used this pattern myself too). Does that crash in your case?
v
I'm quite sure it has crashed for me. I can check if I can make an isolated demo
e
Here’s one of my examples with text field.
Ah yeah a demo/reproducer that would be great to dig into why it crashed 👍🏾
v
What compose version are you using?
e
I see you updated it to include
focusTarget
, that API i am not sure about as I have not used it. Maybe thats your issue?
What compose version are you using?
That code was added in compose 1.3 and has seen both 1.4 & 1.5 upgrades
v
I just added it instead of
clickable
or
focusable
, they both contain it internally
👍🏾 1
e
Okay I see (didnt know that). I just tried adding that explicitly in a preview composable and deployed to device and still no issues (1.5) 👍🏾
v
Hmm, can't seem to make it crash. Not sure what was causing it originally then... 🤔
@Peter seems I was wrong then! I guess you can use
LaunchedEffect(Unit) { f.requestFocus() }
after all
p
No worries, good to know, thanks. I was wondering, why it wasn't causing any problems for me 😄