I have a mutablestate in my ViewModel that I'm trying to set and access in my composable. When I use delegated Property of remember it is working but after creating it in viewModel and accessing it in compose the state of the variable is empty how to use the state with viewModel
Everything is working fine when the state is inside the compose
var mSelectedText by remember { mutableStateOf("") }
But when i use it from viewModel change and set my OutlinedTextField value = mainCatTitle and onValueChange = {mainCatTitle = it} the selected title is not Showing up in the OutlinedTextField is empty
private val _mainCatTitle = mutableStateOf("")
val mainCatTitle: State<String> = _mainCatTitle
my Composable
var mSelectedText by remember { mutableStateOf("") }
var mainCatTitle = viewModel.mainCatTitle.value
Column(Modifier.padding(20.dp)) {
OutlinedTextField(
value = mainCatTitle,
onValueChange = { mainCatTitle = it },
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
mTextFieldSize = coordinates.size.toSize()
},
readOnly = true,
label = { Text(text = "Select MainCategory") },
trailingIcon = {
Icon(icon, "contentDescription",
Modifier.clickable { mExpanded = !mExpanded })
}
)
DropdownMenu(expanded = mExpanded,
onDismissRequest = { mExpanded = false },
modifier = Modifier.width(with(
LocalDensity.current) {
mTextFieldSize.width.toDp()
})) {
selectCategory.forEach {
DropdownMenuItem(onClick = {
mainCatTitle = it.category_name.toString()
mSelectedCategoryId = it.category_id.toString()
mExpanded = false
Log.i(TAG,"Before the CategoryName: $mainCatTitle " )
}) {
Text(text = it.category_name.toString())
}
}
}
}
Log.i(TAG,"Getting the CategoryName: $mainCatTitle " )
}
in my First log inside the DropDownMenuItem the log is showing the Selected field but second log is empty
Have attached the image
Your'e directly modifying the mainCatTitle variable from onClick, not the state hoisted by your ViewMoel
DropdownMenuItem(onClick = {
mainCatTitle = it.category_name.toString()
...
and because you didn't provide anything about your ViewModel, I would assume and suggest to create a function if you don't have one in your ViewModel that you can call like this,
DropdownMenuItem(onClick = {
viewModel.onSelectedItem(it) // <-- this function
...
}
and in your ViewModel you update the state like this
fun onSelectedItem(item: String) { // <-- this is the function
_mainCatTitle.value = item
}
Related
I have a todo list in Jetpack Compose displayed in LazyColumn.
data class TodoItem(val id: Int, val title: String, var urgent: Boolean = false)
val todoList = listOf(
TodoItem(0, "My First Task"),
TodoItem(1, "My Second Task which is really very very long. Super long. I mean longer than a line.", true),
TodoItem(2, "My Third Task"),
)
#Composable
fun MyTodoListView() {
LazyColumn(modifier = Modifier.fillMaxHeight()) {
items(items = todoList, itemContent = { item ->
var checked by remember { mutableStateOf(item.urgent) }
Row(modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.weight(1f).padding(8.dp),
text = item.title)
Checkbox(
checked = checked,
onCheckedChange = {
checked = it
item.urgent = it
}
)
}
})
}
}
When I plan to update the value (through checkbox), I'll have to update is with a separate mutableState variable
onCheckedChange = {
checked = it
item.urgent = it
}
Is there a way to make it more direct, with only one variable to change instead of having to change both checked and item.urgent?
You can use an observable MutableList (like a SnapshotStateList) and then update the items by creating a copy.
Something like:
val todoList = remember {
listOf<TodoItem>(
TodoItem(0, "My First Task"),
//...
).toMutableStateList()
}
LazyColumn(modifier = Modifier.fillMaxHeight()) {
items(items = todoList, itemContent = { item ->
//...
Checkbox(
checked = item.urgent,
onCheckedChange = {
//checked = it
val index = todoList.indexOf(item)
todoList[index] = todoList[index].copy(urgent = it)
}
)
})
}
I have a drop down spinner where i am able to get the category_title . how to get selected category_id i have created a two mutableStateOf of selected title and Id .is there any other way to get the selected category id
var mSelectedText by remember { mutableStateOf("") }
var mSelectedCategoryId by remember { mutableStateOf("") }
data class
data class MainCategory(
var category_id: String? = null,
var category_name: String? = null,
var categoryImage_url: String? = null,
var category_priority:Int? = null
)
list variable
val mCategories = mainCategories.mapTo(arrayListOf()){it.category_name}
DropDownMenu
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun MainCatDownloadContent(
mainCategories: Categories,
mainCategoryOnClick: (categoryId: String, categoryTitle:
String) -> Unit,
) {
var mExpanded by remember { mutableStateOf(false) }
val mCategories = mainCategories.mapTo(arrayListOf()){it.category_name}
var mSelectedText by remember { mutableStateOf("") }
var mSelectedCategoryId by remember { mutableStateOf("") }
var mTextFieldSize by remember { mutableStateOf(Size.Zero) }
// Up Icon when expanded and down icon when collapsed
val icon = if (mExpanded)
Icons.Filled.KeyboardArrowUp
else
Icons.Filled.KeyboardArrowDown
Column(Modifier.padding(20.dp)) {
OutlinedTextField(
value = mSelectedText, onValueChange = { mSelectedText = it },
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
mTextFieldSize = coordinates.size.toSize()
},
readOnly = true,
label = { Text(text = "Select MainCategory")},
trailingIcon = {
Icon(icon,"contentDescription",
Modifier.clickable { mExpanded = !mExpanded })
}
)
mCategories.forEach{ mainCategory ->
DropdownMenuItem(onClick = {
mSelectedText = mainCategory.toString()
mExpanded = false }) {
Text(text = mainCategory.toString())
}
}
}
}
For ex if there is list of items like this
[MainCategory(category_id=11, category_name=Fruits,
categoryImage_url=https:www category_priority=1),
MainCategory(category_id=22, category_name=Vegetable ,
categoryImage_url=https:www, category_priority=2),
MainCategory(category_id=33, category_name=Greens,
categoryImage_url=https:www, category_priority=3)
if selected item lets assume is "Fruits"! how to get its category_id which is "11" and assign to the mSelectedCategoryId variable
as #Gabriele Mariotti says you need to create rememberSaveable State Just elaborating his answer
I assume mainCategories: Categories
// is a List<> so State you can by
val selectCategory by rememberSaveable {
mutableStateOf(mainCategories)
}
Other two variables which you have
var mSelectedText by remember { mutableStateOf("") }
var mSelectedCategoryId by remember { mutableStateOf("") }
Your are missing the DropDownMenu and i assume you just need to show the categoryName and get the selected id of the drop down menu from your question you can do this way
Column(Modifier.padding(20.dp)) {
OutlinedTextField(
value = mSelectedText,
onValueChange = { mSelectedText = it },
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
mTextFieldSize = coordinates.size.toSize()
},
readOnly = true,
label = { Text(text = "Select MainCategory") },
trailingIcon = {
Icon(icon, "contentDescription",
Modifier.clickable { mExpanded = !mExpanded })
}
)
DropdownMenu(expanded = mExpanded,
onDismissRequest = { mExpanded = false },
modifier = Modifier.width(with(
LocalDensity.current) {
mTextFieldSize.width.toDp()
})) {
selectCategory.forEach {
DropdownMenuItem(onClick = {
mSelectedText = it.category_name.toString()
mSelectedCategoryId = it.category_id.toString()
mExpanded = false
}) {
Text(text = it.category_name.toString())
}
}
}
}
Log.i(TAG,"Get the category_name: $mSelectedText and category_id: $mSelectedCategoryId" )
You can use something different.
Add the #Parcelize annotation to the object:
#Parcelize
data class MainCategory(
var category_id: String,
var category_name: String,
) : Parcelable
Then you can use a list of MainCategory and save the state of the selected MainCategory using:
#Composable
fun MainCatDownloadContent(
mainCategories: List<MainCategory>
){
var mExpanded by remember { mutableStateOf(false) }
var selectedCategory by rememberSaveable {
mutableStateOf(mainCategories[0])
}
//...
OutlinedTextField(
value = value = selectedCategory.category_name + " - " + selectedCategory.category_id, //you can access all the properties inside the selectedCategory
//..
)
//..
mainCategories.forEach{ mainCategory ->
DropdownMenuItem(
onClick = {
selectedCategory = mainCategory
mExpanded = false
}
){
//....
}
}
}
Finally in your case is better to use a ExposedDropdownMenuBox + ExposedDropdownMenu as described in this question.
I am learning State hosting in jetpack compose. I am trying to separate my variable in single function and view logic to separate function. But I am getting weird issue in my code. Can someone guide me on this?
PulsePressure
#Composable
fun PulsePressure() {
var systolicTextFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue())
}
var isSystolicTextFieldValueError by rememberSaveable { mutableStateOf(false) }
var diastolicTextFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue())
}
var isDiastolicTextFieldValueError by rememberSaveable { mutableStateOf(false) }
InputWithUnitContainer(
systolicTextFieldValue,
isError = {
isSystolicTextFieldValueError = it
},
incrementTextFieldValue = {
systolicTextFieldValue = it
})
InputWithUnitContainer(
diastolicTextFieldValue,
isError = {
isDiastolicTextFieldValueError = it
},
incrementTextFieldValue = {
diastolicTextFieldValue = it
}
)
}
InputWithUnitContainer
#Composable
fun InputWithUnitContainer(
textFieldValue: TextFieldValue,
isError: (Boolean) -> Unit,
incrementTextFieldValue: (TextFieldValue) -> Unit,
) {
val maxLength = 4
Row(
modifier = Modifier
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
TextField(
value = textFieldValue,
singleLine = true,
onValueChange = {
if (it.text.length <= maxLength) {
incrementTextFieldValue(it)
}
isError(false)
},
isError = isError,
textStyle = RegularSlate20
)
}
}
Error on Textfield
None of the following functions can be called with the arguments supplied.
isError parameter in TextField is a Boolean.
Change 1
You have to change
var isDiastolicTextFieldValueError by rememberSaveable { mutableStateOf(false) }
to
var (isDiastolicTextFieldValueError, updateIsDiastolicTextFieldValueError) = rememberSaveable {
mutableStateOf(
false
)
}
This gives the value and a function to update the value.
Change 2
Then pass both the value and the method to the composable.
Usage
InputWithUnitContainer(
textFieldValue = diastolicTextFieldValue,
isError = isDiastolicTextFieldValueError,
updateIsError = updateIsDiastolicTextFieldValueError,
incrementTextFieldValue = {
diastolicTextFieldValue = it
}
)
Method signature change
#Composable
fun InputWithUnitContainer(
textFieldValue: TextFieldValue,
isError: Boolean,
updateIsError: (Boolean) -> Unit,
incrementTextFieldValue: (TextFieldValue) -> Unit,
) {
...
}
Change 3
Update the usage
TextField(
value = textFieldValue,
singleLine = true,
onValueChange = {
if (it.text.length <= maxLength) {
incrementTextFieldValue(it)
}
updateIsError(false)
},
isError = isError,
textStyle = TextStyle(),
)
I want to display an error message with compose, this works the problem is that the viewModel state call always the state function
I have a Textfield like this
class Test {
#Composable
fun Test() {
val viewModel:TestViewModel = viewModel()
var text by rememberSaveable { mutableStateOf("") }
var isError by rememberSaveable { mutableStateOf(false) }
// liveData
val state by viewModel.viewState.observeAsState(EmailViewState.Nothing)
when (state) {
EmailViewState.OnInvalidPassword -> {
shouldDisplayPasswordError = true
}
/*....*/
}
Column {
TextField(
value = text,
singleLine = true,
isError = isError,
onValueChange = {
text = it
isError = false
},
)
if (isError) {
Text(
text = "Error message",
color = MaterialTheme.colors.error,
style = MaterialTheme.typography.caption,
modifier = Modifier.padding(start = 16.dp)
)
}
}
}
class TestViewModel: ViewModel(){
val viewState = MutableLiveData<EmailViewState>()
fun validate(email:String){
//some validations
viewState.postValue(OnInvalidPassword)
}
}
}
The problem is everytime the recomposition happens, the
val state by viewModel.viewState.observeAsState(EmailViewState.Nothing) is called with the latest value (Invalid) and override the behavior of onValueChange where i set isError = false is any way to combine or avoid the viewState being called in every re composition?
Thanks
I'm trying to implement a TextField in Jetpack Compose with the following functionality: at first it is disabled, but when a user presses the Button, it gets enabled and at the same moment receives focus. This was my approach:
var text by remember { mutableStateOf("text") }
var enabled by remember { mutableStateOf(false)}
val focusRequester = remember { FocusRequester() }
Column {
TextField(
value = text,
onValueChange = { text = it },
enabled = enabled,
modifier = Modifier.focusRequester(focusRequester),
textStyle = TextStyle(fontSize = 24.sp)
)
Button(onClick = {
enabled = true
focusRequester.requestFocus()
}) {
Text("Enable and request focus")
}
But when the button is pressed, the TextField only gets enabled, not focused. To focus it, user has to click it once again. What am I doing wrong and what is the possible workaround?
You have to listen the change of the enabled parameter to give the focus to the TextField.
You can change your code to:
Button(onClick = {
enabled = true
}) {
Text("Enable and request focus")
}
LaunchedEffect(enabled) {
if (enabled){
focusRequester.requestFocus()
}
}
Simply add the delay after enabling textfield like this:
var text by remember { mutableStateOf("text") }
var enabled by remember { mutableStateOf(false)}
val focusRequester = remember { FocusRequester() }
val scope = rememberCoroutineScope()
Column {
TextField(
value = text,
onValueChange = { text = it },
enabled = enabled,
modifier = Modifier.focusRequester(focusRequester),
textStyle = TextStyle(fontSize = 24.sp)
)
Button(onClick = {
scope.launch {
enabled = true
delay(100)
focusRequester.requestFocus()
}
}) {
Text("Enable and request focus")
}
}
I once had a similar issue and I solved it by using interactionSource. The code looked something like this:
var text by remember { mutableStateOf("text") }
var enabled by remember { mutableStateOf(false)}
val focusRequester = remember { FocusRequester() }
val interactionSource = remember { MutableInteractionSource() } // add this
Column {
TextField(
value = text,
onValueChange = { text = it },
enabled = enabled,
modifier = Modifier
.focusable(interactionSource = interactionSource) // and this
.focusRequester(focusRequester),
textStyle = TextStyle(fontSize = 24.sp)
)
...
IIRC it was important to have .focusable() and .focusRequester() in the right order, but cannot remember which exactly, so try to swap them if it won't work immediately.