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
}
}
Related
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")
}
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)
I'm trying to build a custom form using jetpack compose.
What I did so far in the Screen :
#Composable
fun FormContent(
viewModel: FormViewModel,
customFieldList: List<String>,
valuesCustomFieldsList: List<String>
) {
Column(Modifier.fillMaxWidth()) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.weight(1f)
.padding(top = 8.dp, bottom = 8.dp)
) {
if (customFieldList.isNotEmpty()) {
customFieldList.forEachIndexed { index, item ->
TextRow(
title = item,
placeholder = "Insert $item",
value = valuesCustomFieldsList[index],
onValueChange = {
viewModel.onCustomFieldChange(index, it)
},
isError = false
)
}
}
}
}
Where customFieldList is the list of textFields i want and valuesCustomFieldsList is
val valuesCustomFields by viewModel.values.collectAsState()
In my viewModel the code is as it follows:
private val _values = MutableStateFlow(emptyList<String>())
val values : StateFlow<List<String>>
get() = _values
private var customFieldValues = mutableListOf<String>()
init {
viewModelScope.launch {
//get all custom fields
customFields = gmeRepository.getCustomField()
if (customFields.isNotEmpty()) {
//init the customFieldsValues
for (i in 0..customFields.size) {
customFieldValues.add("")
}
_values.value = customFieldValues
}
}
}
fun onCustomFieldChange(index : Int, value: String) {
customFieldValues[index] = value
_values.value = customFieldValues
}
What happens here is that if I try to change the value inside the inputText, onCustomFieldChange is correctly triggered with the first letter I wrote inside, but then no change is visible in the UI.
If I try to change another static field inside the form, then only the last char i wrote inside my custom fields are shown cause a recomposition is triggered.
Is there something I can do to achieve my goal?
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)
},
)
}
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") }
)
}