States in Buttons from LazyVerticalGrid - android-jetpack-compose

I have a data class which provide information of a CarParking Place. This one:
data class ParkingSpace(
val id: Int,
val parkNumber: Int,
val parkState: String
)
fun getParkingSpace() = (1..4).map {
when (it) {
1 -> {
ParkingSpace(
id = it,
parkNumber = 150,
parkState = "Free $it"
)
}
2 -> {
ParkingSpace(
id = it,
parkNumber = 152,
parkState = "Free $it"
)
}
3 -> {
ParkingSpace(
id = it,
parkNumber = 153,
parkState = "Free $it"
)
}
4 -> {
ParkingSpace(
id = it,
parkNumber = 154,
parkState = "Free $it"
)
}
else -> {
ParkingSpace(
id = it,
parkNumber = 150,
parkState = "Free $it"
)
}
}
}
This information is populated in a LazyVerticalGrid through buttons
My goal is:
When you press the button, the Text content of the Button will change into an user name (now is just fixed data) - like saying this park place is reserved to one User
There are two separated buttons, to cancel Reservation. I'm trying to show a different content in the Text's button like "Free". I created this CancelButton for practicing interacting with the content of the buttons.
This is my main Activity with all composables.
class MainActivity : ComponentActivity() {
#ExperimentalCoilApi
#ExperimentalFoundationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ZebraCarPark_ComposeTheme {
ParkingListLayout()
}
}
}
}
#ExperimentalFoundationApi
#ExperimentalCoilApi
#Composable
fun ParkingListLayout() {
Column() {
Box(
modifier = Modifier
.background(Color.LightGray)
.fillMaxWidth()
.padding(horizontal = 0.dp, vertical = 15.dp)
) {
LazyVerticalGrid(
contentPadding = PaddingValues(2.dp),
cells = GridCells.Fixed(4)
) {
items(getParkingSpace()) { parkSpc ->
ParkingPlace(parkSpc)
}
}
}
Divider(
color = Color.Black,
thickness = 2.dp
)
Box(
modifier = Modifier
.background(Color.Green)
) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly
) {
Button(onClick = {}) {
Text(text = "Cancel Park Place 1")
}
Button(onClick = {}) {
Text(text = "Cancel Park Place 2")
}
}
}
}
}
#Composable
fun ParkingPlace(parkingSpace: ParkingSpace) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(
modifier = Modifier.padding(5.dp),
text = "${parkingSpace.parkNumber}",
)
Button(
onClick = { },
colors = ButtonDefaults.buttonColors(Color.Green)
) {
Text(
text = parkingSpace.parkState,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(5.dp)
)
}
}
}
I was able to make some progress using State Hoisting, so pressing a button will change the content of the button with a new value and all of this stuff. The problem is when the cancel button enter in action. I'm not able to handle correctly the states of the content of the Text's button. I prefer to remove all that code to show you this cleaner.
Project result:

Related

Jetpack Compose View Models not updating view

I've created a view model in Jetpack Compose, but the view doesn't refresh when a variable updates inside it.
I've followed the view model guide here: https://developer.android.com/jetpack/compose/libraries#viewmodel
In the SignInWithGoogleView and SignInWithEmailAddressView, the signInOrSignUpMethod variable updates when I click on the ClickableText, but the SignInOrSignUpView doesn't update.
What am I missing?
enum class SignInOrSignUpMethod {
Google,
EmailOnly
}
class SignInOrSignUpViewModel: ViewModel() {
var signInOrSignUpMethod: SignInOrSignUpMethod = SignInOrSignUpMethod.Google
}
//SignInOrSignUpView
#Composable
fun SignInOrSignUpView(signInOrSignUpViewModel: SignInOrSignUpViewModel = viewModel()) {
Box {
//Background image
Image(
painter = painterResource(id = R.drawable.sign_in_sign_up_background),
contentDescription = null,
contentScale = ContentScale.FillBounds,
alpha = 0.35F,
modifier = Modifier
.background(Color.White)
)
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
//App Logo
Image(
painter = painterResource(id = R.drawable.app_logo),
contentDescription = null,
colorFilter = ColorFilter.tint(
(if (isSystemInDarkTheme()) {
Color.White
} else {
Color.Black
})
),
modifier = Modifier
.size(125.dp, 125.dp)
.offset(0.dp, 50.dp)
)
Spacer(modifier = Modifier.height(150.dp))
//Sign In Text
Text(
text = "Sign In",
style = androidx.compose.ui.text.TextStyle(
fontSize = 30.sp,
fontWeight = FontWeight.Bold
)
)
Spacer(modifier = Modifier.weight(1f))
//Sign In Buttons
if (signInOrSignUpViewModel.signInOrSignUpMethod == SignInOrSignUpMethod.Google) {
SignInWithGoogleView()
} else if (signInOrSignUpViewModel.signInOrSignUpMethod == SignInOrSignUpMethod.EmailOnly) {
SignInWithEmailAddressView()
}
}
}
}
#Composable
fun SignInWithGoogleView(signInOrSignUpViewModel: SignInOrSignUpViewModel = viewModel()) {
Text(
text = "Google Sign In",
style = androidx.compose.ui.text.TextStyle(
fontSize = 30.sp,
)
)
ClickableText(
text = AnnotatedString("Continue with email address"),
onClick = {
Log.d("Print", signInOrSignUpViewModel.signInOrSignUpMethod.toString())
signInOrSignUpViewModel.signInOrSignUpMethod = SignInOrSignUpMethod.EmailOnly
}
)
}
#Composable
fun SignInWithEmailAddressView(signInOrSignUpViewModel: SignInOrSignUpViewModel = viewModel()) {
Text(
text = "Email Sign In",
style = androidx.compose.ui.text.TextStyle(
fontSize = 30.sp,
)
)
ClickableText(
text = AnnotatedString("Continue with Google"),
onClick = {
signInOrSignUpViewModel.signInOrSignUpMethod = SignInOrSignUpMethod.Google
}
)
}
This bit I was missing was in my ViewModel to declare mutableStateOf:
class SignInOrSignUpViewModel: ViewModel() {
var signInOrSignUpMethod: SignInOrSignUpMethod by mutableStateOf(SignInOrSignUpMethod.Google)
}

Jetpack Compose animation is jumping

I'm trying to add a fade in/fade out animation to switch between 2 views but the animation is jumping when I run it on the simulator.
I've added x2 AnimatedVisibility to switch between each of the views in my SignInOrSignUpMasterView.
How can I fix this?
class SignInOrSignUp : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
Surface(modifier = Modifier.fillMaxSize()) {
SignInOrSignUpMasterView()
}
}
}
}
}
//--------------------------------------------------------------------------------------------------
//SignInOrSignUpMethod enum
enum class SignInOrSignUpMethod {
Google,
EmailOnly
}
//--------------------------------------------------------------------------------------------------
//SignInOrSignUpViewModel
class SignInOrSignUpViewModel : ViewModel() {
var signInOrSignUpMethod: SignInOrSignUpMethod by mutableStateOf(SignInOrSignUpMethod.EmailOnly)
}
//--------------------------------------------------------------------------------------------------
//SignInOrSignUpMasterView
#Composable
fun SignInOrSignUpMasterView(
signInOrSignUpViewModel: SignInOrSignUpViewModel = viewModel()
) {
Box {
//Background image
Image(
painter = painterResource(id = R.drawable.sign_in_sign_up_background),
contentDescription = null,
contentScale = ContentScale.Crop,
alpha = 0.50F,
modifier = Modifier
.background(Color.White)
.fillMaxSize()
)
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
//App Logo
Image(
painter = painterResource(id = R.drawable.app_logo),
contentDescription = null,
colorFilter = ColorFilter.tint(
(if (isSystemInDarkTheme()) {
Color.Black
} else {
Color.White
})
),
modifier = Modifier
.size(125.dp, 125.dp)
.offset(0.dp, 50.dp)
)
//Spacer to fill the rest of screen to position the buttons at the bottom of the screen
Spacer(modifier = Modifier.weight(1f))
//Sign In/Up Views
AnimatedVisibility(
visible = signInOrSignUpViewModel.signInOrSignUpMethod == SignInOrSignUpMethod.Google,
enter = fadeIn(),
exit = fadeOut()
) {
SignInOrSignUpWithGoogleView()
}
AnimatedVisibility(
visible = signInOrSignUpViewModel.signInOrSignUpMethod == SignInOrSignUpMethod.EmailOnly,
enter = fadeIn(),
exit = fadeOut()
) {
SignInOrSignUpWithEmailAddressView()
}
Spacer(Modifier.height(10.dp))
Text("Legal information")
Spacer(Modifier.height(40.dp))
}
}
}
//--------------------------------------------------------------------------------------------------
//SignInOrSignUpWithGoogleView
#Composable
fun SignInOrSignUpWithGoogleView(
signInOrSignUpViewModel: SignInOrSignUpViewModel = viewModel()
) {
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
Text("Sign in with Google view")
ClickableText(
text = AnnotatedString("Continue with email address"),
onClick = {
signInOrSignUpViewModel.signInOrSignUpMethod = SignInOrSignUpMethod.EmailOnly
},
style = TextStyle(
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
color = if (isSystemInDarkTheme()) {
Color.Black
} else {
Color.White
}
)
)
}
}
//--------------------------------------------------------------------------------------------------
//SignInOrSignUpWithEmailAddressView
#Composable
fun SignInOrSignUpWithEmailAddressView(
signInOrSignUpViewModel: SignInOrSignUpViewModel = viewModel()
) {
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
Text("Sign in with email address view")
ClickableText(
text = AnnotatedString("Continue with Google"),
onClick = {
signInOrSignUpViewModel.signInOrSignUpMethod = SignInOrSignUpMethod.Google
},
style = TextStyle(
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
color = if (isSystemInDarkTheme()) {
Color.Black
} else {
Color.White
}
)
)
}
}
You Should Wrap your animated visibility in a box because if they are in a column the column will allocate space for the showing view while the other one is fading away resulting in a janky animation.
Box {
// Wrap your Animated Visibility in a Box
// Sign In/Up Views
androidx.compose.animation.AnimatedVisibility(
visible = signInOrSignUpViewModel.signInOrSignUpMethod == SignInOrSignUpMethod.Google,
enter = fadeIn(),
exit = fadeOut()
) {
SignInOrSignUpWithGoogleView()
}
androidx.compose.animation.AnimatedVisibility(
visible = signInOrSignUpViewModel.signInOrSignUpMethod == SignInOrSignUpMethod.EmailOnly,
enter = fadeIn(),
exit = fadeOut()
) {
SignInOrSignUpWithEmailAddressView()
}
}
Update
Box {
val googleAlpha by animateFloatAsState(
if (signInOrSignUpViewModel.signInOrSignUpMethod == SignInOrSignUpMethod.Google) 1f
else 0f
)
val emailAlpha by animateFloatAsState(
if (signInOrSignUpViewModel.signInOrSignUpMethod == SignInOrSignUpMethod.EmailOnly) 1f
else 0f
)
SignInOrSignUpWithGoogleView(
modifier = Modifier.alpha(googleAlpha)
)
SignInOrSignUpWithEmailAddressView(
modifier = Modifier.alpha(emailAlpha)
)
}

Call popup composable function inside onclick of Button in jetpack compose

Hi i want to show popup when something happens . I have this popup:
#Composable
fun popup(message:String,height:Dp,width:Dp,icon:String=""){
Column() {
val openDialog = remember { mutableStateOf(true) }
val dialogWidth = width/(1.3F)
val dialogHeight = height/2
if (openDialog.value) {
Dialog(onDismissRequest = { openDialog.value = false }) {
// Draw a rectangle shape with rounded corners inside the dialog
Box(
Modifier
.size(dialogWidth, dialogHeight)
.background(Color.White)){
Column(modifier = Modifier.fillMaxWidth().padding()) {
Text(text = message)
}
}
}
}
Button(onClick = {
openDialog.value=!openDialog.value
}) {
}
}
}
But i am trying to call him inside onclick Button event :
Button(modifier = Modifier
.padding(start = 6.dp, end = 6.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = Azul99),
onClick = {
if (vm.validateCredentials()=="ok"){
vm.createUser()
}else{
popup(vm.validateCredentials(),200.dp,200.dp,"fill")
}
},
shape = RoundedCornerShape(percent = 28)
) {
Text(text = "Registrarme",
modifier= Modifier.fillMaxWidth(),
style= TextStyle(fontWeight = FontWeight.Bold),
color= Color.White,
textAlign = TextAlign.Center)
}
and Android Studio says: "#Composable invocations can only happen from the context of a #Composable function" How can i call the popup ??
Store showPopUp boolean as state and show popUp by that state;
val showPopUp by remember { mutableStateOf(false)} // -> STATE
Button(
modifier = Modifier
.padding(start = 6.dp, end = 6.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = Azul99),
onClick = {
if (vm.validateCredentials()=="ok"){
vm.createUser()
}else{
showPopUp = !showPopUp // -> CHANGE IN HERE
}
},
shape = RoundedCornerShape(percent = 28)
) {
Text(
text = "Registrarme",
modifier= Modifier.fillMaxWidth(),
style= TextStyle(fontWeight = FontWeight.Bold),
color= Color.White,
textAlign = TextAlign.Center
)
}
if(showPopUp){
popup(vm.validateCredentials(),200.dp,200.dp,"fill") // -> SHOW HERE
}
Change

Keyboard does not hide, when focused TextField leaves the LazyColumn

Perhaps this is the normal behaviour, but i wish it was different. I had tried to google the solution, but did not find anything suitable (or merely missed it).
Sample code (for simplicity i hold mutable states right here, not using ViewModel):
#Composable
fun Greeting() {
Scaffold(topBar = {
TopAppBar(title = { Text(text = "Some title") })
}) {
val focusManager = LocalFocusManager.current
LazyColumn(
contentPadding = PaddingValues(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
items(count = 20) { index ->
val (value, onValueChange) = rememberSaveable { mutableStateOf("Some value $index") }
TextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier.fillMaxWidth(),
label = { Text(text = "Some label $index") },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = {
if (!focusManager.moveFocus(FocusDirection.Down))
focusManager.clearFocus()
}),
singleLine = true
)
}
}
}
}
Compose version 1.0.5
You could try just hiding the keyboard whenever scrolling occurs. This is okay as long as you don't have a large set of items. But since you're using TextFields, it isn't likely that you'll have such a large number. This sample illustrates hiding the keyboard when scrolling occurs:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
Greeting(this)
}
}
}
#Composable
fun Greeting(activity: Activity) {
Scaffold(topBar = {
TopAppBar(title = { Text(text = "Some title") })
}) {
val lazyListState = rememberLazyListState()
val ctx = LocalContext.current
LaunchedEffect(lazyListState.firstVisibleItemIndex) {
val inputMethodManager = ctx.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(activity.window?.decorView?.windowToken, 0)
}
LazyColumn(
state = lazyListState,
contentPadding = PaddingValues(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
items(
count = 20,
key = { index ->
// Return a stable + unique key for the item
index
}
) { index ->
val (value, onValueChange) = rememberSaveable { mutableStateOf("Some value $index") }
TextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier
.fillMaxWidth(),
label = { Text(text = "Some label $index") },
singleLine = true
)
}
}
}
}

Select all text of TextField in Jetpack Compose with mvvm pattern

In the previous text field, when focused, there was a method in which all letters were selected. I found a way to make it remembered on the screen, but I wonder how to do it on the mvvm pattern.
#Composable
fun MainScreen(text: String, viewModel: HomeViewModel) {
val textState = remember { mutableStateOf(TextFieldValue()) }
val state = viewModel.mainState.text.collectAsState()
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = state.value,
color = Color.Blue,
fontSize = 40.sp
)
Button(
onClick = { viewModel.mainState.text.value = "New text" },
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Green
),
modifier = Modifier.padding(16.dp)
) {
Text(text)
}
TextField(
value = textState.value,
onValueChange = { textState.value = it },
label = { Text("Input text") }
)
}
}
The code above is from screen to remeber.
But I understand that remember is only declared within #Composable.
The view model does not declare #Composable, so I want to know how to do it in the mvvm pattern.
Below is my code.
LoginScreen
val text = viewModel.user_id.value
OutlinedTextField(
value = barcode,
onValueChange = {
viewModel.changeBarcode(it)
},
modifier = Modifier
.fillMaxWidth()
.padding(all = 4.dp)
.onFocusChanged { focusState ->
if (focusState.isFocused) {
//monitor value
}
},
label = { Text(text = "Barcode") },
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
keyboardController?.hide()
viewModel.onTriggerEvent(MenuStateEvent.ScanEvent)
}
)
)
LoginViewModel
val user_id: MutableState<String> = mutableStateOf("")
How change it to mvvm pattern?
Maybe this will be the method you need.
in ViewModel:
val user_id = mutableStateOf(TextFieldValue(""))
in Compose:
TextField(
value = viewModel.user_id.value,
onValueChange = { viewModel.user_id.value = it },
label = { Text("Input text") }
)
Just change the mutableStateOf in ViewModel from "String" to "TextFieldValue(String)" to access it.

Resources