Jetpack Compose TextField blur event - android

I'm implementing form controls, with validations etc. I want an error message to be shown only when a user "blurs" the TextField, in other words, when the field loses its focus. In Angular, we have touched state that we can proceed from. How to listen for losing focus state in Jetpack Compose?
#Composable
fun Screen() {
TextField(
onBlur = {
// P.S. This parameter does not exist
}
)
}

You can use onFocusChanged.
Sample Code:
var color by remember { mutableStateOf(Black) }
Box(
Modifier
.border(2.dp, color)
// The onFocusChanged should be added BEFORE the focusable that is being observed.
.onFocusChanged { color = if (it.isFocused) Green else Black }
.focusable()
)
Update Answer:
TextField API update - merged onFocus and onBlur callbacks into a single onFocusChange(Boolean) callback with parameter
Source: Version 0.1.0-dev15 - July 22, 2020

If anyone is still having this issue, I solved this using #snorlax answer, but improving on it. My problem was a TextField data validation. The TextField is by nature focusable, different from a Box, so adding .focusable() after defining the event was not doing what I planned: the validation happened once the component appeared on screen, and obviously we didn't want that.
On the .onFocusChanged modifier, we can check the event types and use it to change the blurred state, triggering then the validation.
I had a validate() function and a validState boolean state that I used between my presenter and my view, but I'll make it simple here and you may ignore the implementation of the state class:
We have the focus events, and we can emulate a blur event with a simple flag:
class FieldValidState {
var value: Boolean by mutableStateOf(true)
}
#Composable
fun TextFieldValidation(validState: FieldValidState = remember { FieldValidState() })
Column {
var isBlurred = false
TextField(
Modifier
.onFocusChanged {
if (!it.isFocused && isBlurred) validState.value = validate()
if (it.isFocused && !isBlurred) isBlurred = true }
)
}
}
So what happens here is: I want the validation function to be triggered after lost focus on the TextField. On startup, the field is inactive (i.e. it.isFocused == false) but I don't want to validate then. So once the focus is on the field and it was not blurred before, isBlurred becomes true and when it loses focus, it will be validated.
Let me know if this helps! Thanks!

Related

Jetpack Compose: Focus Styling

I am making custom components and with my custom components I am required to cover the focus state.
At the top of my component hierarchy I am tracking focus with this.
val focused = remember { mutableStateOf(false) }
val focusModifier = modifier.onFocusEvent {
focused.value = it.hasFocus || it.isFocused
}
Component(modifier = focusModifier, focused = focused.value)
The component is basically this:
#Composable
fun Component(
modifier: Modifier = Modifier,
focused: Boolean = false
) {
...
val colorStuff = if(focused) focusColors else otherColors
var focusModifier = modifier
if(focused) {
focusModifier = modifier.border(BorderStroke(2.dp, Color.Red)).padding(16.dp)
}
NextComponent(
focusModifier,
colorStuff,
etc
)
}
If I leave the code with colorStuff and focused without the focusModifier code the focus state is done correctly and the colors for the component change appropriately. But when I add the focusModifier code and do a border and padding the focus state will trigger, but then instantly be lost. I'm assuming this is because the addition of modifier code changes the build order of the component and makes it discard focus. But that doesn't make full sense either.
I essentially need to add borders/shadows around components when they are focused so this will be something I have to do multiple times. Right now I can't get it done once. Any idea what I need to do to overcome this?
This is not an answer to your question, but I'll share my use-case, maybe it could help. Consider the codes below
data class PersonItem(
val id : Int,
val isActive: Boolean = false
)
List Item
#Composable
fun PersonInfoItem() {
Column {
// First Name
TextField(
modifier = Modifier.onFocusEvent {
// report to view model this personItem received focus events
},
value = "",
onValueChange = {
}
)
// Last Name
TextField(
modifier = Modifier.onFocusEvent {
// report to view model this personItem received focus events
},
value = "",
onValueChange = {
}
)
}
VieModel
class ViewModel {
val currentFocusedItem : PersonItem? = null
fun onPersonItemFocused(personItem: PersonItem) {
// this is where I validate that if the personItem argument
// is not the same with the currentFocusedPerson, if they are not then the last one is not on focus anymore (set !isActive)
// otherwise if they are the same, we are still on the same PersonItem
}
Regardless of which TextField received or lost its focus, as long as I'm keeping track of the Id of the PersonItem being reported to the ViewModel I'm guaranteed that if its the same PersonItem, it will retain what ever state I put into it during its Focus events
(say, the item will change background color when either of the TextField received focus, and revert back to its original background when none of the TextField is focused).
Another thing to consider, these kinds components (i.e TextFields) report multiple inactive focus events during its initial composition, and it's kind of annoying.

Why recomposition happens when call ViewModel in a callback?

I completely confused with compose conception.
I have a code
#Composable
fun HomeScreen(viewModel: HomeViewModel = getViewModel()) {
Scaffold {
val isTimeEnable by viewModel.isTimerEnable.observeAsState()
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
.background(Color.Black),
) {
Switch(
checked = isTimeEnable ?: false,
onCheckedChange = {
viewModel.setTimerEnable(it)
},
)
Clock(viewModel.timeSelected.value!!) {
viewModel.setTime(it)
}
}
}
}
#Composable
fun Clock(date: Long, selectTime: (date: Date) -> Unit) {
NumberClock(Date(date)) {
val time = SimpleDateFormat("HH:mm", Locale.ROOT).format(it)
Timber.d("Selected time: time")
selectTime(it)
}
}
Why Clock widget recomposes when I tap switch. If I remove line selectTime(it) from Clock widget callback recomposition doesn't happen.
Compose version: 1.0.2
This is because in terms of compose, you are creating a new selectTime lambda every time, so recomposition is necessary. If you pass setTime function as a reference, compose will know that it is the same function, so no recomposition is needed:
Clock(viewModel.timeSelected.value!!, viewModel::setTime)
Alternatively if you have more complex handler, you can remember it. Double brackets ({{ }}) are critical here, because you need to remember the lambda.
Clock(
date = viewModel.timeSelected.value!!,
selectTime = remember(viewModel) {
{
viewModel.setTimerEnable(it)
}
}
)
I know it looks kind of strange, you can use rememberLambda which will make your code more readable:
selectTime = rememberLambda(viewModel) {
viewModel.setTimerEnable(it)
}
Note that you need to pass all values that may change as keys, so remember will be recalculated on demand.
In general, recomposition is not a bad thing. Of course, if you can decrease it, you should do that, but your code should work fine even if it is recomposed many times. For example, you should not do heavy calculations right inside composable to do this, but instead use side effects.
So if recomposing Clock causes weird UI effects, there is probably something wrong with your NumberClock that cannot survive the recomposition. If so, please add the NumberClock code to your question for advice on how to improve it.
This is the intended behaviour. You are clearly modifying the isTimeEnabled field inside your viewmodel when the user toggles the switch (by calling vm.setTimeenabled). Now, it is apparent that the isTimeEnabled in your viewmodel is a LiveData instance, and you are referring to that instance from within your Composable by calling observeAsState on it. Hence, when you modify the value from the switch's onValueChange, you are essentially modifying the state that the Composable depends on. Hence, to render the updated state, a recomposition is triggered

onKeyEvent Modifier doesn't work in Jetpack Compose

return ComposeView(requireContext()).apply {
setContent {
Box(
Modifier
.onKeyEvent {
if (it.isCtrlPressed && it.key == Key.A) {
println("Ctrl + A is pressed")
true
} else {
false
}
}
.focusable()
)
}
}
Why the key event cannot be called in fragment while using hardware keyboard of tablet?
As documentation of onKeyEvent says:
will allow it to intercept hardware key events when it (or one of its children) is focused.
Which means you need to make your box focused, not just focusable. To do this you need a FocusRequester, in my example I'm asking focus when view renders. Check out more in this article
For the future note, that if user taps on a text field, your box will loose focus, but onKeyEvent still gonna work if this txt field is inside the box
Looks like 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 {
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
This code works fine with my Bluetooth keyboard + android smartphone, emulator seems not recognizing CTRL

Jetpack Compose: Not able to show text in TextField

Recently I'm playing with Jetpack Compose and I noticed that the text may not show up in TextField.
So I have a ViewModel with Flow of ViewState.
In my Compose file, I have something similar to this:
#Composable
internal fun TestScreen() {
val state by viewModel.state.collectAsState()
TestScreen {
viewState = state,
actioner = { ... }
}
}
#Composable
private fun TestScreen(viewState: ViewState, actioner: () -> Unit) {
var name by remember {
mutableStateOf(
TextFieldValue(viewState.name)
)
}
Surface {
....
Column {
....
OutlinedTextField(
...
value = name,
onValueChange = { textFieldValue ->
name = textFieldValue
actioner(...)
}
)
}
}
}
the OutlineTextField will never show what's already inside viewState.name
However, if I change this:
var name by remember {
mutableStateOf(
TextFieldValue(viewState.name)
)
}
To this:
var name = TextFieldValue(viewState.name)
Obviously it could show the value in viewState.name.
According to the Documentation (https://developer.android.com/jetpack/compose/state#state-in-composables) in which it recommends using remember & mutableStateOf to handle the changes.
I'll be very grateful if someone could help me to explain why the code with remember doesn't work but the directly assigned value worked?
EDIT
viewState.name is a String
and I "partially solved" this issue by doing the following:
var name by remember {
mutableStateOf(
TextFieldValue("")
)
}
name = TextFieldValue(viewState.name)
then the name can be shown. But it doesn't look quite right?
remember is used just to ensure that upon recomposition, the value of the mutableStateOf object does not get re-initialised to the initial value.
For example,
#Composable
fun Test1(){
var text by mutableStateOf ("Prev Text")
Text(text)
Button(onClick = { text = "Updated Text" }){
Text("Update The Text")
}
}
would not update the text on button click. This is because button click will change the mutableStateOf text, which will trigger a recomposition. However, when the control reaches the first line of the Composable, it will re-initialise the variable text to "Prev Text".
This is where remember comes in.
If you change the initialisation above to
var text by remember { mutableStateOf ("Prev Text") },
It wil tell compose to track this variable, and "remember" its value, and use it again on recomposition, when the control reaches the initialisation logic again. Hence, remember over there acts as a "guard" that does not let the control reach into the initialisation logic, and returns that latest remembered value of the variable it currently has in store.

Compose TextField clears value on gaining focus

I have a Composable function which displays 2 TextFields. Here's my code:
fun CreateEntryItem() {
var wordA by remember { mutableStateOf("") }
var wordB by remember { mutableStateOf("") }
Column {
Row {
TextField(
value = wordA,
onValueChange = { wordA = it },
enabled = true,
modifier = Modifier.weight(1f)
)
TextField(
value = wordB,
onValueChange = { wordB = it },
enabled = true,
modifier = Modifier.weight(1f)
)
}
}
}
When I give focus to TextField A, I can type and the value of wordA updates correctly.
Here's the weird behaviour:
I then give focus to TextField B. I then give focus back to TextField A. When I start typing, instead of TextField A inserting / appending characters at the cursor position in the existing text, it completely clears the existing text (as set pre-focus), and 'starts afresh'. That is to say, each TextField only remembers text entered in the current 'focus session'.
Am I doing this wrong? Or is this a bug in Compose? I reproduced this behaviour on both 1.0.0-beta07 and 1.0.0-beta08.
Turns out this isn't a Compose bug per se, but an emulator issue. Running the exact same code on a real device didn't run into this issue.
The emulator that encountered this issue was API 30 running on macOS Big Sur. I haven't tested whether other systems are also affected by this emulator bug.

Categories

Resources