Variable doesn't update inside composable using jetpack compose - android

I'm trying out jetpack compose and have a basic textfield composable that accepts a boolean variable for validation.
However it doesn't update properly and only works at initialisation.
class RegistrationActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var name = "zzz"
BasicField(
title = "Last Name",
value = name,
onValueChange = {name = it},
placeholder = "Enter Last Name",
validation = (name.length>2&&name.contains("zzz"))
)
}
}}
composable:
#Composable
fun BasicField(title: String,
value: String,
onValueChange: (String) -> Unit,
placeholder: String,
maxLines: Int = 1,
validation: Boolean ) {
var fieldValue by remember { mutableStateOf(TextFieldValue(value)) }
BasicTextField(
maxLines = maxLines,
value = fieldValue,
onValueChange = {
fieldValue = it
if(fieldValue.text.length>5){//this part works
Log.e("error","valid string")
}else{
Log.e("error","invalid string")
}
if(validation){//this part doesnt work
Log.e("error","custom validation works")
}else{
Log.e("error","custom validation failed")
}
},
) }
I have a basic validation inside the composable which checks for string length which works, but when the logic is from outside it doesn't work. I appreciate any help or hint thanks!

You need to make your name as state in compose. Here we have used 2 things.
name is defined as state. And it is remembered in composition. So whenever this composable function goes into re-composition, this name will not be re-assigned.
Used LaunchedEffect to execute your logs statements. So LaunchedEffect will start with a key and when they key changes, this effect will restart.
class RegistrationActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// 1
val name = remember { mutableStateOf("") }
BasicField(
title = "Last Name",
value = name.value,
onValueChange = {name.value = it},
placeholder = "Enter Last Name",
validation = (name.value.length>2&&name.value.contains("zzz"))
)
}
}}
#Composable
fun BasicField(title: String,
value: String,
onValueChange: (String) -> Unit,
placeholder: String,
maxLines: Int = 1,
validation: Boolean ) {
// 2
LaunchedEffect(key = value) {
if(value.length>5){
Log.e("error","valid string")
}else{
Log.e("error","invalid string")
}
if(validation){
Log.e("error","custom validation works")
}else{
Log.e("error","custom validation failed")
}
}
BasicTextField(
maxLines = maxLines,
value = value,
onValueChange = {
onValueChange(it)
},
)
}

Related

onValueChange of BasicTextField is not triggered on setting value to TextFieldValue("") in Jetpack Compose

I want to execute some code when the value of BasicTextfield changes in Jetpack Compose.
Everything works fine in 2 conditions:
for any value change.
if all the textfield value is cleared using the device keyboard
But,
When I try to change the state value to empty text on click of a button, using this code :
textfieldstate.value = TextFIeldValue("")
onValueChange is not triggered.
Although if I set it to any other value, onValueChange is triggered.
textfieldstate.value = TextFIeldValue("FOO")
Code of Button/Icon click:
Icon(modifier = Modifier.clickable {
textfieldstate.value = TextFieldValue("")
}) {.....}
Is there a way to trigger onValueChange of BasicTextField when value of the field is cleared from an external button click event??
If you want to do it all at once as is more recommended, I would do this:
#Composable
fun AppContent(
viewModel: MyViewModel
) {
val state by viewModel.uiState.collectAsState()
MyPanel(
state = MyViewModel,
onValueChange = viewModel::onValueChange,
onClickButton = viewModel::onClickButton
)
}
#Composable
fun MyPanel(
state: MyTextFieldState,
onValueChange: (String) -> Unit,
onClickButton: () -> Unit
) {
TextField(
value = state.text,
onValueChange = onValueChange(it)
)
Button(
onClick = { onClickButton() }
) {
...
}
}
class MyViewModel: ViewModel() {
private val _uiState = MutableStateFlow(MyUiState())
val uiState = _uiState.asStateFlow()
fun onValueChange(str: String) {
_uiState.value = _uiState.value.copy(text = str)
}
fun onClickButton() {
_uiState.value = _uiState.value.copy(text = "")
}
}
data class MyUiState(
val text: String = ""
)
The code above mainly elevates the state of the TextField, processes all things in the viewModel, and wraps a layer of UI state with a data class. If there are other requirements, you can also add different parameters, for example, if there is an error in the TextField, it can be written as:
data class MyUiState(
val text: String = "",
val isTextError: Boolean = false
)
The onValueChange callback is useful to be informed about the latest state of the text input by users.
If you want to trigger some action when the state of (textFieldValue) changes, you can use a side effect like LaunchedEffect.
Something like:
var textFieldValue by remember() {
mutableStateOf(TextFieldValue("test" ))
}
LaunchedEffect(textFieldValue) {
//doSomething()
}
BasicTextField(
value = textFieldValue,
onValueChange = {
textFieldValue = it
}
)
OutlinedButton(
onClick = { textFieldValue = textFieldValue.copy("") }
) {
Text(text = "Button")
}

Jetpack Compose: mutableStateOf doesn't update with flow

I have a ViewModel which uses Flow to get a Note object from my Room database:
var uiState by mutableStateOf(NoteUiState())
private set
private fun getNoteById(argument: Int) {
viewModelScope.launch {
try {
repository.getNoteById(argument).collect { note ->
uiState = NoteUiState(note = note)
}
} catch (e: Exception) {
uiState = NoteUiState(error = true)
}
}
}
Note class:
#Entity(tableName = "notes")
data class Note(
#PrimaryKey(autoGenerate = true) val id: Int = 0,
#ColumnInfo(name = "title") val title: String = "",
#ColumnInfo(name = "text") val text: String = "",
) {
override fun toString() = title
}
This approach works fine, until I try to make a mutable strings with the values of the Note object as their default so I can update 2 TextField composables:
#OptIn(ExperimentalMaterial3Api::class)
#Composable
private fun Note(
note: DataNote,
isNewNote: Boolean,
createNote: (DataNote) -> Unit,
updateNote: (DataNote) -> Unit,
back: () -> Unit,
trued: String,
) {
var title by remember { mutableStateOf(note.title) }
var content by remember { mutableStateOf(note.text) }
TextField(
value = title,
onValueChange = { title = it },
modifier = Modifier
.fillMaxWidth(),
placeholder = { Text(text = "Title") },
colors = TextFieldDefaults.textFieldColors(
containerColor = Color.Transparent
)
)
TextField(
value = content,
onValueChange = { content = it },
modifier = Modifier
.fillMaxWidth(),
placeholder = { Text(text = "Content") },
colors = TextFieldDefaults.textFieldColors(
containerColor = Color.Transparent
)
)
}
For some reason the first time the Note object is called it's null, so I want a way to update the title and content variables.
The Note object itself updates without issue, however the title and content variables never change from the initial value. How can I update the title and content variables while also making them work for the textfield?
I found out how to make the Textfield work while also getting the inital value from the object. The issue was that the Note object was called as null on the first call, so the mutableStateFlow didnt get the initial values.
First, I had to pass the actual state as a MutableStateFlow to my composable:
#Composable
private fun Note(
state: MutableStateFlow<NoteUiState>,
createNote: (DataNote) -> Unit,
updateNote: (DataNote) -> Unit,
back: () -> Unit
) {
...
Next, I just had to get the Note object by calling collectAsState():
val currentNote = state.collectAsState().value.note
Finally, all that was needed was to pass the currentNote object text and title in the value of the Textfield, and on onValueChange to update the state object itself via a copy:
This is the complete solution:
#OptIn(ExperimentalMaterial3Api::class)
#Composable
private fun Note(
state: MutableStateFlow<NoteUiState>,
createNote: (DataNote) -> Unit,
updateNote: (DataNote) -> Unit,
back: () -> Unit
) {
val currentNote = state.collectAsState().value.note
Column(Modifier.fillMaxSize()) {
TextField(
value = currentNote.title,
onValueChange = {
state.value = state.value.copy(note = currentNote.copy(title = it))
},
modifier = Modifier
.fillMaxWidth(),
placeholder = { Text(text = "Title") },
colors = TextFieldDefaults.textFieldColors(
containerColor = Color.Transparent
)
)
}
}
I'm not sure is this is a clean solution, but is the only way it worked for me, thanks for the feedback, opinions on this approach are always welcomed.
You should think about state hoisting and about having a single source of truth.
Really you need to define where your state will live, if on the viewmodel or on the composable functions.If you are only going to use the state (your note) on the ui then it's ok to hoist your state up to the Note composable function.
But if you need that state in something like another repo, insert it somewhere else or in general to do operations with it, the probably you should hoist it up to the viewmodel (you already have it there).
So use the property of your viewmodel directly in your composable and add a function in your viewmodel to mutate the state and pass this function to the onValueChanged lambda.
var title by remember { mutableStateOf(note.title) }
var content by remember { mutableStateOf(note.text) }
remember block executes only on 1st composition and then value will remembered until decomposition or u need to change it externally through '=' assignment operator,
.
instated of this
TextField(
value = title)
write this way
TextField(
value = note.title)

Listeners inside detectTapGestures only ever get the first variable value - Other Compose views update as expected

I am trying to copy a text passed into a Compose view on long press. It is not directly the value, but instead, it's something like itemText ?: "Fallback". The thing is, the value of the parameter text inside the onLongPress lambda is never updated. Only the first value gets taken. This was both evaluated debugging & using toasts to display the value.
Also tried out many other things, including not using remember, using a function instead of directly using itemText ?: "Fallback", etc.
Inside the Button's onClickListener, the value is always up to date.
Am I doing something wrong or is there a Compose bug in there somewhere? As I am guessing that it's a bug, I have reported it to the issue tracker: https://issuetracker.google.com/issues/216160969
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeTutoriaTheme {
Sample()
}
}
}
}
#Composable
fun Sample() {
var itemText: String? by remember {
mutableStateOf(null)
}
Column {
Button(
modifier = Modifier.padding(top = 10.dp, bottom = 20.dp),
onClick = {
itemText = if (itemText == null) {
"Tomato"
} else {
null
}
}
) {
Text(text = "Tap to toggle")
}
GroceryItem(text = itemText ?: "Fallback")
}
}
#Composable
fun GroceryItem(text: String) {
val context = LocalContext.current
Text(
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onLongPress = {
Toast.makeText(context, text, Toast.LENGTH_LONG).show()
}
)
},
text = text
)
Button(
onClick = {
Toast.makeText(context, text, Toast.LENGTH_LONG).show()
}
) {
Text(text = "Button to copy from, onClick")
}
}
This is expected behavior.
Modifier.pointerInput remembers all variables, just like remember does. This is done so as not to interrupt touch handling during any recomposition.
And same as with remember, you can pass any parameters you need to keep track of as a key parameter instead of Unit:
Modifier.pointerInput(text) {
detectTapGestures(
onLongPress = {
Toast.makeText(context, text, Toast.LENGTH_LONG).show()
}
)
},

Setting initial value of TextField from Flow in Compose

I have a TextField component that I want to set the initial content to. The initial content will be fetched from a database using a Flow.
I have this TextField code (loosely following this codelabs tutorial):
#Composable
private fun EntryText(placeholder: String, initialText: String = "", ) {
val (text, setText) = remember { mutableStateOf(initialText) }
EntryTextField(
text = text,
placeholder = placeholder,
onTextChanged = setText
)
}
and
#Composable
private fun EntryTextField(text: String, placeholder: String, onTextChanged: (String) -> Unit) {
TextField(
modifier = Modifier.fillMaxWidth(),
value = text,
onValueChange = {
onTextChanged(it)
},
placeholder = { Text(text = placeholder) }
)
}
I want use it like so to set both a Text and the content of the EntryText:
val entry by viewModel.getEntryContent().collectAsState(initial = "initial")
val hint = "hint"
Column {
Text(text = entry)
EntryText(placeholder = hint, initialText = entry)
}
When the ViewModel getEntryContent flow emits the result from the database only the Text is being updated with the new String and not the EntryText (it stays with the initial state of "initial").
How can I have my TextField get updated when my ViewModel emits the string?
Because your text is handle by ViewModel, I think you can store it state in ViewModel like
class MainViewModel : ViewModel() {
var entry = MutableStateFlow("initial")
fun getEntryContent(): Flow<String> {
// get from database
}
}
In your Activity
val entry by viewModel.entry.collectAsState()
Column {
Text(text = entry)
EntryText(placeholder = hint, text = entry, onTextChanged = {
viewModel.entry.value = it
})
}
lifecycleScope.launch {
viewModel.getEntryContent().flowWithLifecycle(lifecycle).collect {
// observe value from db then set to TextField
viewModel.entry.value = it
}
}

TextField with Kotlin StateFlow

I'd like to have a TextField bound to a MutableStateFlow that comes from a view model. This is how I set it up:
#Composable
fun MyTextField(textFlow: MutableStateFlow<String>) {
val state = textFlow.collectAsState(initial = "")
TextField(
value = TextFieldValue(state.value),
onValueChange = { textFlow.value = it.text },
label = { Text(text = "Label") }
)
}
When I type something into the text field, it behaves really strangely. For example, if I type 'asd', it ends up with 'asdasa'. How can I update textFlow.value without messing up with the text field?
This error is caused by the usage of TextFieldValue with Flow.
To fix this, set the value of the TextField to just state.value and then on text change set the value with textFlow.value = it.
#Composable
fun MyTextField(textFlow: MutableStateFlow<String>) {
val state = textFlow.collectAsState(initial = "")
TextField(
value = state.value,
onValueChange = { textFlow.value = it },
label = { Text(text = "Label") }
)
}

Categories

Resources