#Composable funck. into onClick event - Jetpack Compose - android

I try show AlertDialog when press a button.
For AlertDialog i have a composable function - showDialog.
It is clear that this function calls a dialog.
I have another composable function which displays some window with text and buttons.
When the button is clicked, I want to call a function that stores the AlertDialog.
AlertDialog body:
fun monthDialog() {
val openDialog = remember { mutableStateOf(true) }
if (openDialog.value) {
AlertDialog(
onDismissRequest = {
openDialog.value = false
},
title = {
Text(text = "Title")
},
text = {
Text(
"This area typically contains the supportive text " +
"which presents the details regarding the Dialog's purpose."
)
},
buttons = {
Row(
modifier = Modifier.padding(all = 8.dp),
horizontalArrangement = Arrangement.Center
) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { openDialog.value = false }
) {
Text("Dismiss")
}
}
}
)
}
first I tried the simplest solution that came to my mind:
IconButton(onClick = monthDialog())
and got error (#Composable invocations can only happen from the context of a #Composable function)
after i tried a normal trigger using mutablestateof and following condition:
val showDialog = remember { mutableStateOf(false)}
if(showDialog.value == true) monthDialog()
and put some like this into onClick event:
monthHeader(onClick = {showDialog.value = !showDialog.value})
and this is work....but ugly and badly.
for a first time this is worf fine. but after the first click, I need to click two more times to trigger that event again.
button code snippet:
fun Calendar (){
//...
val showDialog = remember { mutableStateOf(false)}
if(showDialog.value == true) monthDialog()
Card(
){
Column(){
IconButton(onClick ={
monthDialog() // error here
//showDialog.value = !showDialog.value
}
}
}
after few hours for searching in google
i try my own solution
val clicked = remember { mutableStateOf(false)}
val showDialog = remember { mutableStateOf(false)}
if(showDialog.value) monthDialog()
else if(clicked.value) monthDialog()
Card(){
Column(){
monthHeader(onClick = {
clicked.value = showDialog.value
showDialog.value = !clicked.value
})
}
}
but in my opinion this is crutch/kludge

Leaving a better solution here (imho):
Hoist the state of your dialog.
#Composable
fun MonthDialog(onClose: () -> Unit) {
AlertDialog(
onDismissRequest = onClose,
title = {
Text(text = "Title")
},
text = {
Text(
"This area typically contains the supportive text " +
"which presents the details regarding the Dialog's purpose."
)
},
buttons = {
Row(
modifier = Modifier.padding(all = 8.dp),
horizontalArrangement = Arrangement.Center
) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = onClose
) {
Text("Dismiss")
}
}
}
)
Noticed that I removed the state from this component and make it stateless. This component will just notify when the dialog is closed.
Now you can call the dialog like this:
var showDialog by remember { mutableStateOf(false) }
if (showDialog) {
MonthDialog(onClose = { showDialog = false })
}
Card {
MonthHeader( // use upper case for naming your composables
onClick = {
showDialog = true
}
)
}

One can also use Modifier / PointerInputScope:
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = { },
onTap = { },
onDoubleTap = { },
onLongPress = { }
)

Related

Display multiple dialog types with different text in Compose

Which whould be the best approach to display, for example, 2 types of dialogs on a screen and also parametrize the message to each dialog? For now I have a remember mutable state DialogType enum variable which represents if a dialog must be displayed or not, so if the state is MessageDialog or ConfirmationDialog will display one of those. But... what if I need to parametrize the message (or more variables like for example a image, but for this case it will be simplyfied to message) to one of these dialogs? Should I use another remember state variable with the string? If so, then I'm using two different remember state variables. Is that correct?
For sure there are complex ways to achieve this, creating special data classes, wrapping them, passing composable content as parameters to the dialog, etc... but I'm trying to avoid these kind of complex and overprogrammed developments and trying to find a simple and easy way to achieve this.
This is the sample code I did:
var dialogState by remember { mutableStateOf(DialogStateType.HIDDEN) }
var dialogMessage by remember { mutableStateOf("") }
In the buttons i change the state of these variables like this:
onClick = {
dialogMessage = "sample message text"
dialogState = DialogStateType.MESSAGE
}) {
onClick = {
dialogMessage = "sample confirmation dialog text"
dialogState = DialogStateType.CONFIRMATION
}) {
And in the main composable I display them using this:
when (dialogState) {
DialogStateType.MESSAGE -> {
MessageDialog(dialogMessage) {
dialogState = DialogStateType.HIDDEN
}
}
DialogStateType.CONFIRMATION -> {
ConfirmationDialog(dialogMessage,
{
CoroutineScope(Dispatchers.Default).launch {
//DO HARD JOB
}
},
{ dialogState = DialogStateType.HIDDEN },
{ dialogState = DialogStateType.HIDDEN })
}
DialogStateType.HIDDEN -> {}
}
This approach works and it's simple, but seems to be not too much escalable, Is this the recommended way of doing this? or is there a simpler and easier way?
Kotlin's default arguments and ?.let {...} can help
Some class representing state:
data class DialogState(val caption: String? = "Dialog",
val text: String? = null,
val imageResId: String? = null,
val confirmButtonText: String = "OK",
val cancelButtonText: String? = null,
val onConfirm: () -> Unit = {},
val onCancel: () -> Unit = {})
Generic dialog composabe like:
#Composable
fun GenericDialog(dialogState: DialogState, onDismiss: () -> Unit) {
Dialog(onDismissRequest = onDismiss) {
Surface {
Column(
modifier = Modifier.padding(10.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
dialogState.caption?.let {
Text(text = dialogState.caption,
style = TextStyle(fontWeight = FontWeight.ExtraBold))
Spacer(modifier = Modifier.size(10.dp))
}
dialogState.imageResId?.let{
//show image
Spacer(modifier = Modifier.size(10.dp))
}
dialogState.text?.let {
Text(text = dialogState.text)
Spacer(modifier = Modifier.size(10.dp))
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Button(onClick = dialogState.onConfirm) {
Text(text = dialogState.confirmButtonText)
}
dialogState.cancelButtonText?.let {
Spacer(modifier = Modifier.size(10.dp))
Button(onClick = dialogState.onCancel) {
Text(text = dialogState.cancelButtonText)
}
}
}
}
}
}
}
And then:
var showDialog by remember { mutableStateOf(false) }
var dialogState = remember { DialogState() }
//...
if (showDialog) GenericDialog(dialogState = dialogState) { showDialog = false }
//...
Button(onClick = {
dialogState = DialogState(caption = "Caption",
text = "Text text text text text text text text text text text",
onConfirm = {showDialog = false})
showDialog = true
}) {
Text(text = "Caption, text, one button")
}
// or
Button(onClick = {
dialogState = DialogState(caption = null,
text = "Text text text text text text text text text text text",
cancelButtonText = "Cancel",
onConfirm = {showDialog = false},
onCancel = {showDialog = false})
showDialog = true
}) {
Text(text = "Text, two buttons")
}

Animate visibility in compose

I have a text which need to be animated to show and hide with the value is null or not. it would have been straight forward if the visibility is separately handle, but this is what I got.
In the bellow code the enter animation works but the exit animation dont as the text value is null.
I can think of something with remembering the old value but not sure how.
#Composable
fun ShowAnimatedText(
text : String?
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
AnimatedVisibility(
visible = text != null,
enter = fadeIn(animationSpec = tween(2000)),
exit = fadeOut(animationSpec = tween(2000))
) {
text?.let {
Text(text = it)
}
}
}
}
I think the fade-out animation is actually working "per-se".
I suspect the parameter text: String? is a value coming from a hoisted "state" somewhere up above ShowAnimatedText, and since you are directly observing it inside the animating scope, when you change it to null it instantly removes the Text composable, and your'e not witnessing a slow fade out.
AnimatedVisibility(
...
) {
text?.let { // your'e directly observing a state over here
Text(text = it)
}
}
This is my attempt completing your snippet based on my assumption and making it work, the fade-in works, but the desired fade-out is instantly happening.
#Composable
fun SomeScreen() {
var text by remember {
mutableStateOf<String?>("Initial Value")
}
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(onClick = {
text = "New Value"
}) {
Text("Set New Value")
}
Button(onClick = {
text = null
}) {
Text("Remove Value")
}
AnimatedText(text = text)
}
}
#Composable
fun ShowAnimatedText(
text : String?
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
AnimatedVisibility(
visible = text != null,
enter = fadeIn(animationSpec = tween(2000)),
exit = fadeOut(animationSpec = tween(2000))
) {
text?.let {
Text(text = it)
}
}
}
}
You can solve it by modifying the text to a non-state value and change your visibility logic from using a nullability check to some "business logic" that would require it to be visible or hidden, modifying the codes above like this.
#Composable
fun SomeScreen() {
var show by remember {
mutableStateOf(true)
}
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(onClick = {
show = !show
}) {
Text("Set New Value")
}
AnimatedText(text = "Just A Value", show)
}
}
#Composable
fun ShowAnimatedText(
text : String?,
show: Boolean
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
AnimatedVisibility(
visible = show,
enter = fadeIn(animationSpec = tween(2000)),
exit = fadeOut(animationSpec = tween(2000))
) {
text?.let {
Text(text = it)
}
}
}
}
I fixed it by remembering the previous state (Or don't set the null value) until the exit animation is finished, if the text is null.
Thank you z.y for your suggestion.
#Composable
fun ShowAnimatedText(
text : String?,
show: Boolean
) {
var localText by remember {
mutableStateOf<String?>(null)
}
AnimatedContent(show, localText)
LaunchedEffect(key1 = text, block = {
if(text == null){
delay(2000)
}
localText = text
})
}
#Composable
private fun AnimatedContent(show: Boolean, localText: String?) {
Column(
modifier = Modifier.fillMaxWidth()
) {
AnimatedVisibility(
visible = show,
enter = fadeIn(animationSpec = tween(2000)),
exit = fadeOut(animationSpec = tween(2000))
) {
localText?.let {
Text(text = it)
}
}
}
}

SimpleAlertDialog component in Jetpack Compose cannot be added to a DetailView due to context of #Composable

I was developing an app where I'm using Jetpack Compose as UI developing tool, and I design some kind of custom AlertDialog, which is the following:
CustomAlertDialog.kt
#Composable
fun SimpleAlertDialog(
hero: CharacterModel? = null,
show: Boolean,
onConfirm: () -> Unit,
onDismiss: () -> Unit,
textDescription: String,
textTittle: String,
) {
if(show){
AlertDialog(
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = onConfirm)
{ Text(text = "OK") }
},
dismissButton = {
TextButton(onClick = onDismiss)
{ Text(text = "Cancel") }
},
title = { Text(text = textTittle) },
text = { Text(text = textDescription) }
)
}
}
But when I try to use in a detailScreen, I get the following context Composable error:
#Composable invocations can only happen from the context of a #Composable function
the region of code where I try to instances the following, where I get the error:
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun MyCharactersListRowView(
viewmodel: MainViewModel,
characterModel: CharacterModel,
popBack: () -> Unit
) {
val (characterSelected, setCharacterSelected) = remember { mutableStateOf<CharacterModel?>(null) } //HOOK FUNCTION
val openDialog = remember { mutableStateOf(false) }
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = { setCharacterSelected(characterModel) })
.padding(vertical = 8.dp, horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
){
AsyncImage(
model = characterModel.image,
contentDescription = characterModel.name,
contentScale = ContentScale.Fit,
modifier = Modifier
.size(60.dp)
.clip(CircleShape)
.combinedClickable(
onLongClick = {
if (characterSelected != null) {
SimpleAlertDialog(
hero = characterSelected,
show = true,
onConfirm = {
viewmodel
.deleteCharacter(characterSelected!!.id.toLong())
.also {
Log.d(
"info",
"rowAffected: ${viewmodel.rowAffected.value}"
)
if (viewmodel.rowAffected.value.toInt() != 0) {
Toast
.makeText(
LocalContext.current!!,
"ยก ${characterSelected.name} bought sucessfully!",
Toast.LENGTH_LONG
)
.show()
.also {
openDialog.value = false
}
} else {
Toast
.makeText(
LocalContext.current!!,
viewmodel.loadError.value,
Toast.LENGTH_LONG
)
.show()
.also {
openDialog.value = false
}
}
}
},
onDismiss = { openDialog.value = false },
textTittle = "Remove character",
textDescription = "Would you like to remove ${characterSelected.name} from your characters?"
)
} else {
}
},
onClick = {
//TODO {Do something}
}
)
)
...
...
So I know is a quite beginner error, but I've not been able to get into a working solution, due to take thanks in advance is you know how implement it.
You can't call a Dialog from a lambda that doesn't have #Composable annotation. You can check this answer out for differences between Composable and non-Composable functions.
fun Modifier.combinedClickable(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onLongClickLabel: String? = null,
onLongClick: (() -> Unit)? = null,
onDoubleClick: (() -> Unit)? = null,
onClick: () -> Unit
)
These lambdas are not #Composable
Common way for showing dialog in Jetpack Compose is
var showDialog by remember {mutableStateOf(false)}
if(characterSelected && showDialog) {
SimpleAlertDialog(onDismiss={showDialog = false})
}
and change showDialog to true inside long click
onLongClick = {
showDialog = true
}
Show custom alert dialog in Jetpack Compose

Avoid accidental tap/touch in a Jetpack Compose Slider which is placed inside in a scrollable column

I have a Slider which is placed inside a Column which is scrollable. When i scroll through the components sometimes accidentally slider value changes because of accidental touches. How can i avoid this?
Should i be disable taps on slider? If yes how can i do it?
Is there any alternate like Nested scroll instead of Column which can prevent this from happening?
#Composable
fun ColumnScope.FilterRange(
title: String,
range: ClosedFloatingPointRange<Float>,
rangeText: String,
valueRange: ClosedFloatingPointRange<Float>,
onValueChange: (ClosedFloatingPointRange<Float>) -> Unit,
) {
Spacer(modifier = Modifier.height(Size_Regular))
Text(
text = title,
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.height(Size_X_Small))
Text(
text = rangeText,
style = MaterialTheme.typography.subtitle1
)
RangeSlider(
modifier = Modifier.fillMaxWidth(),
values = range,
valueRange = valueRange,
onValueChange = {
onValueChange(it)
})
Spacer(modifier = Modifier.height(Size_Small))
Divider(thickness = DividerSize)
}
I would disable the RangeSlider and only enable it when you tap on it. You disable it by tapping anywhere else within the Column. This is a similar behavior used to mimic losing focus. Here's an example:
class MainActivity : ComponentActivity() {
#ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
var rangeEndabled by remember { mutableStateOf(false)}.apply { this.value }
var sliderPosition by remember { mutableStateOf(0f..100f) }
Text(text = sliderPosition.toString())
Column(modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.pointerInput(Unit) {
detectTapGestures(
onTap = {
rangeEndabled = false
}
)
}) {
repeat(30) {
Text(it.toString())
}
RangeSlider(
enabled = rangeEndabled,
values = sliderPosition,
onValueChange = { sliderPosition = it },
valueRange = 0f..100f,
onValueChangeFinished = {
// launch some business logic update with the state you hold
// viewModel.updateSelectedSliderValue(sliderPosition)
},
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onTap = {
rangeEndabled = true
}
)
}
)
repeat(30) {
Text(it.toString())
}
}
}
}
}

Why FocusManager doesn't work inside AlertDialog?

Does anyone knows how can I show or hide the keyboard inside AlertDialog?
The focusManager.clearFocus() doesn't work inside AlertDialog.
Same for textInputService?.hideSoftwareKeyboard() and softwareKeyboardController?.hide().
For example:
AlertDialog(
onDismissRequest = {
openDialog.value = false
},
text = {
TextField(...)
}
buttons = {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { focusManager.clearFocus() }
) {
Text("Update")
}
}
)
The AlertDialog, as any other Dialog, has its own LocalFocusManager as well as some other local constants.
You are capturing its value outside of AlertDialog, instead you need to capture it inside:
buttons = {
val focusManager = LocalFocusManager.current
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { focusManager.clearFocus() }
) {
Text("Update")
}
}

Categories

Resources