Jetpack Compose: Focus Styling - android

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.

Related

Jetpack Compose: Provide initial value for TextField

I want to achieve the following use case: A payment flow where you start with a screen to enter the amount (AmountScreen) to pay and some other screens to enter other values for the payment. At the end of the flow, a summary screen (SummaryScreen) is shown where you can modify the values inline. For the sake of simplicity we will assume there is only AmountScreen followed by SummaryScreen.
Now the following requirements should be realized:
on AmountScreen you don't loose your input on configuration change
when changing a value in SummaryScreen and go back to AmountScreen (using system back), the input is set to the changed value
AmountScreen and SummaryScreen must not know about the viewModel of the payment flow (PaymentFlowViewModel, see below)
So the general problem is: we have a screen with an initial value for an input field. The initial value can be changed on another (later) screen and when navigating back to the first screen, the initial value should be set to the changed value.
I tried various approaches to achieve this without reverting to Kotlin flows (or LiveData). Is there an approach without flows to achieve this (I am quite new to compose so I might be overlooking something obvious). If flows is the correct approach, would I keep a MutableStateFlow inside the PaymentFlowViewModel for amount instead of a simple string?
Here is the approach I tried (stripped and simplified from the real world example).
General setup:
internal class PaymentFlowViewModel : ViewModel() {
var amount: String = ""
}
#Composable
internal fun NavigationGraph(viewModel: PaymentFlowViewModel = viewModel()) {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "AMOUNT_INPUT_SCREEN"
) {
composable("AMOUNT_INPUT_SCREEN") {
AmountInputRoute(
// called when the Continue button is clicked
onAmountConfirmed = {
viewModel.amount = it
navController.navigate("SUMMARY_SCREEN")
},
// apply the entered amount as the initial value for the input text
initialAmount = viewModel.amount
)
}
composable("SUMMARY_SCREEN") {
SummaryRoute(
// called when the amount is changed inline
onAmountChanged = {
viewModel.amount = it
},
// apply the entered amount as the initial value for the input text
amount = viewModel.amount
)
}
}
}
The classes of the AmountScreen look like this:
#Composable
internal fun AmountInputRoute(
initialAmount: String,
onAmountConfirmed: (String) -> Unit
) {
// without the "LaunchedEffect" statement below this fulfils all requirements
// except that the changed value from the SummaryScreen is not applied
val amountInputState: MutableState<String> = rememberSaveable { mutableStateOf(initialAmount) }
// inserting this fulfils the req. that the changed value from SummaryScreen is
// applied, but breaks keeping the entered value on configuration change
LaunchedEffect(Unit) {
amountInputState.value = initialAmount
}
Column {
AmountInputView(
amountInput = amountInputState.value,
onAmountChange = { amountInput ->
amountInputState.value = amountInput
}
)
Button(onClick = { onAmountConfirmed(amountInputState.value) }) {
Text(text = "Continue")
}
}
}
```
I achieved the goal with a quite complicated approach - I would think there are better alternatives out there.
What I tried that did not work: using rememberSaveable passing initialAmount as parameter for inputs. Theoretically rememberSaveable would reinitialize its value when inputs changes, but apparently this does not happen when the composable is only on the back stack and also is not executed when it gets restored from the back stack.
What I implemented that did work:
#Composable
internal fun AmountInputRoute(
initialAmount:String,
onAmountConfirmed: (String) -> Unit
) {
var changedAmount by rememberSaveable {
mutableStateOf<String?>(null)
}
val amountInput by derivedStateOf {
if (changedAmount != null)
changedAmount
else
initialAmount
}
AmountInputView(
amountInput = amountInput,
onContinueClicked = {
onAmountConfirmed(amountInput)
changedAmount = null
},
validAmountChanged = {
changedAmount = it
}
)
}
Any better ideas?

reveal parts of text without causing a recomposition?

I'm basically writing a small quiz game with android jetpack compose in which you've got a question displayed and a text field below, I wanted to implement a "hint" (which appears under the text field and shows more and more of the correct answer for every bad answer). After implementing it, it causes a recomposition which selects a new question. Thus a question arrives, is there a way to update just the "hint" part of the screen or is that impossible ? (this might sound stupid but maybe I've missed something and there is a way)
Thanks in advance for every comment :)
You can recompose any part of a UI (without recomposing the entire screen or unrelated parts of the screen) as long as you isolate that section and only apply state changes to those parts that you want updated. The value you apply to your hint text should come from a viewmodel that contains a mutable state variable containing the text you want to update the hint with:
class MyViewModel : ViewModel() {
var hintText = mutableStateOf("")
fun onTextFieldChange(answer: String) {
// Retrieve your hint text from whatever api handles the user's response.
hintText.value = processAnswer(answer)
}
}
#Composable
fun Question() {
val vm = MyViewModel()
var text by remember { mutableStateOf("") }
Column(modifier = Modifier.fillMaxWidth()) {
TextField(
value = text,
onValueChange = {
text = it
vm.onTextFieldChange(it)
},
label = { Text("Label") },
singleLine = true
)
HintText(vm = vm)
}
}
#Composable
fun HintText(
vm: MyViewModel
) {
Text(text = vm.hintText.value)
}

Jetpack Compose TextField blur event

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!

Composable reparenting in Jetpack Compose

Is there a way to reparent a Composable without it losing the state? The androidx.compose.runtime.key seems to not support this use case.
For example, after transitioning from:
// This function is in the external library, you can not
// modify it!
#Composable
fun FooBar() {
val uid = remember { UUID.randomUUID().toString() }
Text(uid)
}
Box {
Box {
FooBar()
}
}
to
Box {
Row {
FooBar()
}
}
the Text will show a different message.
I'm not asking for ways to actually remember the randomly generated ID, as I could obviously just move it up the hierarchy. What I want to archive is the composable keeping its internal state.
Is this possible to do without modifying the FooBar function?
The Flutter has GlobalKey specifically for this purpose. Speaking Compose that might look something like this:
val key = GlobalKey.create()
Box {
Box {
globalKey(key) {
FooBar()
}
}
}
Box {
Row {
globalKey(key) {
FooBar()
}
}
}
This is now possible with
movableContentOf
See this example:
val boxes = remember {
movableContentOf {
LetterBox(letter = 'A')
LetterBox(letter = 'B')
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = { isRow = !isRow }) {
Text(text = "Switch")
}
if (isRow) {
Row(
Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
boxes()
}
} else {
Column(
Modifier.weight(1f),
verticalArrangement = Arrangement.Center
) {
boxes()
}
}
}
remember will store only one value in the same view. The key in Compose has a very different purpose: if the key passed to remember has a different value from the last recomposition, it means that the old value is no longer relevant and must be recomputed.
There is no direct equivalent of Flutter keys in Compose.
You can simply declare a global variable. In case you need to change it, wrap it with a mutable state, so changes will update your view.
var state by mutableStateOf(UUID.randomUUID().toString())
I'm not sure if that the same what GlobalKey does, in any case it's not the best practice, just like any other global variable.
If you need to share some data between views, it is much cleaner to use view models.
#Composable
fun TestScreen() {
val viewModel = viewModel<SomeViewModel>()
Column {
Text("TestScreen text: ${viewModel.state}")
OtherView()
}
}
#Composable
fun OtherView() {
val viewModel = viewModel<SomeViewModel>()
Text("OtherScreen text: ${viewModel.state}")
}
class SomeViewModel: ViewModel() {
var state by mutableStateOf(UUID.randomUUID().toString())
}
The hierarchy topmost viewModel call creates a view model - in my case inside TestScreen. All children that call viewModel of the same class will get the same object. The exception to this is different destinations of Compose Navigation, see how to handle this case in this answer.
You can update the mutable state value, and it will be reflected on all views using that model. Check out more about state in Compose.
When the view that created the view model is removed from the view hierarchy, the view model is also freed, so a new one will be created next time.

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.

Categories

Resources