How to retrigger focusRequester.requestFocus() when coming back to a composable? - kotlin

I have a MyBasicTextField in a composable to request user input:
#Composable
fun MyBasicTextField() {
val keyboardController = LocalSoftwareKeyboardController.current
val focusRequester = remember{ FocusRequester() }
BasicTextField(
modifier = Modifier
.focusRequester(focusRequester),
keyboardActions = keyboardActions ?: KeyboardActions(onAny = { keyboardController?.hide() }),
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
The keyboard automatically slides in when showing this composable, always.
But wherever MyBasicTextField is used:
I tap on a LinkifiedText to leave and open a browser to show link
I tap BACK
and come back to previous MyBasicTextField screen, the keyboard is not shown
also the focusRequester.requestFocus() is not triggered again when coming back
How can I solve my issue?

Create a top-level variable in your activity, then modify it from within the onStart overridden method. Use that variable as the key for LaunchedEffect in place of Unit. That variable basically keeps track of when the user enters the app.
var userIn by mutableStateOf (true)
In your Composable,
Launched effect(userIn){
if(userIn && isKeyboardShown){
...
}
}
Boring,

You can use my answer here as well, but instead of incrmenting the launchKey every time it's called, only increment the launchKey once user clicks on the browser link, that way it will not pop up during other re-compositions.

Related

How to detect focus move direction on focus change?

I have a Composable Row that has some click listeners:
val action = { ... }
Row(Modifier.clickable(action) {
IconButton({ /* other, unrelated action */}) {}
Text("This isn't clickable")
Checkbox({ /* something that calls action() on toggle */ })
}
When tabbing through this UI, the focus goes to the IconButton, then the Checkbox, then the Row. I want it to skip the row. I've implemented that by adding to the Row modifier:
val manager = LocalFocusManager.current
Row(Modifier.clickable(action).onFocusChanged {
if (it.isFocused) manager.moveFocus(FocusDirection.Next)
}) { /* same content */ }
... which works when moving forward, but not when moving backward (using Shift-Tab). And of course that's because of the FocusDirection.Next, which should instead be Previous when moving backward. But how do I detect that? The focus event doesn't have a direction property.
Update
I tried doing this by manually detecting if shift is pressed, which feels more like a hack than a solution:
val keys = LocalWindowInfo.current.keyboardModifiers
/* in onFocusChanged */
manager.moveFocus(if (keys.isShiftPressed) FocusDirection.Previous else FocusDirection.Next)
.. and also, it doesn't work. Calling manager.moveFocus(FocusDirection.Previous) if shift is pressed causes an infinite loop and application crash, presumably because it's setting the focus back to where it came from.

Composable State from Jetpack Compose execute these two states once or twice

I got a two states for handling a dynamic pop up screen component
var showPopUpScreen by remember { viewModel.popUpScreenIsOpen }
var popUpType by remember { viewModel.popUpScreenType }
but when I change the value of these mutableState-values when opening the pop up component
like this:
fun OpenPopUpScreen(type: BasePopUpScreen) {
popUpScreenType.value = type
popUpScreenIsOpen.value = true
}
will this composable function get executed twice (which is performance heavy) or will it be smart enough to know that these values are set at once so execute my pop up render function only once?
Extra code info:
fun LiveTrainingScreen(viewModel: LiveTrainingViewModel = viewModel()) {
// lots of code and then:
var showPopUpScreen by remember { viewModel.popUpScreenIsOpen }
var popUpType by remember { viewModel.popUpScreenType }
//pop up container
if(showPopUpScreen) {
Row(modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.6f))
.zIndex(11f), verticalAlignment = Alignment.CenterVertically) {
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
DyanmicPopUpScreenLiveTraining(popUpScreenTypeInfo = popUpType, viewModel = viewModel)
} // pop up main column
} // end pop up screen row
} // end if pop up screen
}
I believe the recomposition starts right after both have changed as the compose guide says:
"Recomposition is optimistic and may be canceled." [source: https://developer.android.com/jetpack/compose/mental-model]
in which means the recomposition will be canceled as the other parameter is assigned and you will see the change in state in UI in means of both values.
However, it is a better approach to save the UI state inside a data class and remember the data class directly. that way, you change both variables and the composition resets as the data class changes. plus, rather than remembering the data class, hoist the state in a ViewModel and you will good to go.
I think compose is smart enough to identify the changes and react on it.
As per your question
once you set first value it will start changes compose views which are dependent on it
And suppose considering complex view previous recomposition process is going on, after setting second value previous recomposition will get cancelled and compose will recompose your screen with updated both values.
So effectively we can Recomposition will happen once only.

Composable Focus

I have a project where we display a graph, this graph is in an Box which is scrollable.
By opening the view, we need to center the root node which causes the problem currently.
Determining the position and setting the values of the states is currently done the following way:
.onGloballyPositioned { coordinates -> scrollBy = coordinates.positionInParent().y - dpstate!!.scrollState.firstVisibleItemScrollOffset
}
.onFocusChanged {
if(it.isFocused ){
print("is focused")
scope.launch { dpstate!!.scrollState.animateScrollBy(scrollBy)
dpstate!!.offsetState.value = Offset(leftX.toFloat(),dpstate!!.offsetState.value.y)
}
}
}
in the modifier of the box.
The state dpstate is an instance of the following:
data class DisplayState(
val scrollState: LazyListState,
val scaleState: MutableState<Float>,
val offsetState: MutableState<Offset>,
val editState: MutableState<Boolean>,
val showInfo:Map<Int, MutableState<Boolean>>,)
Important is, that I need to center by opening it, not by clicking a button.
My try was in the calling code of all of this the following code:
DisposableEffect(Unit){
com.github.tukcps.appel.ui.rendering.focusRequester!!.requestFocus()
onDispose { }
}
There is no exception, it just don't do anything, thanks for your help.

How to trigger PC Keyboard inputs in Kotlin Desktop Compose

I am going to develop a POS system using Kotlin Jetpack Compose and I wanna know how to trigger keyboard input events inside my project.
In Compose Desktop You can listen for key events using onKeyEvent Window parameter:
Window(
onCloseRequest = ::exitApplication,
visible = visible,
onKeyEvent = {
if (it.isCtrlPressed && it.key == Key.A) {
println("Ctrl + A is pressed")
true
} else {
// let other handlers receive this event
false
}
}
) {
App()
}
An other options, which will also work for Compose in Android, is using Modifier.onKeyEvent. As documentation says:
will allow it to intercept hardware key events when it (or one of its children) is focused.
So you need to make an item or one of its children focusable and focused. Check out more about focus in compose in this article
To do this you need a FocusRequester, in my example I'm asking focus when view renders using LaunchedEffect.
For the future note, that if user taps on a text field, or an other focusable element will gain focus, your view will loose it. If this focused view is inside your view with onKeyEvent handler, it still gonna work.
An empty box cannot become focused, so you need to add some size with a modifier. It still will be invisible:
val requester = remember { FocusRequester() }
Box(
Modifier
.onKeyEvent {
if (it.isCtrlPressed && it.key == Key.A) {
println("Ctrl + A is pressed")
true
} else {
// let other handlers receive this event
false
}
}
.focusRequester(requester)
.focusable()
.size(10.dp)
)
LaunchedEffect(Unit) {
requester.requestFocus()
}
Alternatively just add content to Box so it will stretch and .size modifier won't be needed anymore
Following the second option of the Philip answer is possible to get a strange behavior when you set the focus and, for some reason, click inside application window. Doing this, is possible "lost" the focus and the key events are not propper handled.
In order to avoid this the suggestion is manually handle this problem by adding a click/tap modifier, which just specifies that when detect a click/tap the requester requests the focus again. See below:
val requester = FocusRequester()
Box(
Modifier
//pointer input handles [onPress] to force focus to the [requester]
.pointerInput(key1 = true) {
detectTapGestures(onPress = {
requester.requestFocus()
})
}
.onKeyEvent {
if (it.isCtrlPressed && it.key == Key.A) {
println("Ctrl + A is pressed")
true
} else {
// let other handlers receive this event
false
}
}
.focusRequester(requester)
.focusable()
.fillMaxSize()
.background(Color.Cyan)
)
LaunchedEffect(Unit) {
requester.requestFocus()
}

How to open (enter) SettingsFragment or any Fragment on Android (Kotlin)

I am learning Android programing, and I can not figure it out how to open settings fragment when button is clicked.
This fragment is not in the navigation map, so it is not possible to connect them in navigation and to use findNavController().navigate(actionFromTo)
As explained on developer guide: https://developer.android.com/guide/topics/ui/settings
I have created fragment:
class PreferencesFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
}
}
What I need to write in click listener to enter settings fragment?
Click listener is:
binding.buttonSettings.setOnClickListener {
}
I have tried with to use code in developers guide:
binding.buttonSettings.setOnClickListener {
val fragmentManager: FragmentManager? = activity?.supportFragmentManager
fragmentManager?.beginTransaction()?.replace(R.id.preferencesFragment, PreferencesFragment())?.commit()
}
But program crashes when button is pressed, with error:
No view found for...
The issue seems to be the content ID you want to replace, therefore you need to pass the current content ID you want to replace
see below code, use android.R.id.content instead of R.id.preferencesFragment
binding.buttonSettings.setOnClickListener {
val fragmentManager: FragmentManager? = activity?.supportFragmentManager
fragmentManager?.beginTransaction()?.replace(android.R.id.content, PreferencesFragment())?.commit()
}