How can I fix my alert dialog in kotlin compose? - android

I' ve been making a dictionary app for a while and I added that users can create own dictionaries on my app. I show users dictionary on the screen and users can delete their dictionaries whatever they want. so I am trying to make alert dialog for this because I want users not to delete their dictionaries when they press the delete icon directly. An alert dialog will appear on the screen and there should be two buttons such as cancel and accept in that alert dialog. If the user presses accept, that is, if he wants to delete, I want the dictionary to be deleted.
However, the problem is that it is difficult to implement this in compose and in the codes I wrote because I encountered many problems for some reason, whereas it should have been easy. What I did in my codes is that if user clicks delete icon onDeleteClick works and showAlertDialog becomes true in onDeleteClick. When true, it goes inside the top if block and calls the alert dialog component. When the alert dialog compo is called, CustomDialogUI opens. I send two parameters to CustomDialogUI, one is a showAlertDialog mutablestate that controls the opening and closing of the alert dialog, and the second one is deleteDicState if the user says allow in the alert dialog that opens, deleteDicState becomes true and if deleteDicState is true, the deletion must occur.
Since deleteDicState is false the first time, it does not delete, but when the alert dialog opens for the second time and I say delete, it deletes it for some reason. How can i solve this problem help.
my code
#Composable
fun CreateYourOwnDictionaryScreen(
navController: NavController,
viewModel: CreateYourOwnDictionaryViewModel = hiltViewModel()
) {
val scaffoldState = rememberScaffoldState()
val state = viewModel.state.value
val scope = rememberCoroutineScope()
val context = LocalContext.current
val showAlertDialog = remember { mutableStateOf(false) }
val deleteDicState = remember { mutableStateOf(false) }
if(showAlertDialog.value){
Dialog(onDismissRequest = { showAlertDialog.value = false }) {
CustomDialogUI(openDialogCustom = showAlertDialog,deleteDicState)
}
}
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
backgroundColor = bar,
title = {
androidx.compose.material3.Text(
text = "your dictionaries",
modifier = Modifier.fillMaxWidth(),
color = Color.White,
fontSize = 22.sp
)
},
navigationIcon = {
IconButton(onClick = {
navController.navigate(Screen.MainScreen.route)
}) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Go Back"
)
}
}
)
},
floatingActionButtonPosition = FabPosition.Center,
floatingActionButton = {
FloatingActionButton(
onClick = { navController.navigate(Screen.CreateDicScreen.route) },
backgroundColor = bar,
) {
Icon(Icons.Filled.Add, "fab")
}
}
) {
Box(modifier = Modifier.background(MaterialTheme.colors.background)) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(state.dictionaries) { dictionary ->
CreateYourOwnDictionaryItem(
dictionary = dictionary,
modifier = Modifier
.fillMaxWidth()
.clickable {
navController.navigate(Screen.MyWordsScreen.passDicId(dictionary.uid))
},
onAddClick = {
navController.navigate(
Screen.MakeYourDictionaryScreen.passDicId(
dictionary.uid
)
)
},
onDeleteClick = {
if(deleteDicState.value){
viewModel.onEvent(
CreateYourOwnDictionaryEvents.DeleteDictionary(dictionary)
)
scope.launch {
val result = scaffoldState.snackbarHostState.showSnackbar(
message = "dictionary is deleted",
/*actionLabel = "Undo",*/
duration = SnackbarDuration.Short
)
}
}
},
onEditClick = {
navController.navigate(
Screen.UpdateOwnDictionaryScreen.passDicIdAndDicName(
dictionary.uid,
dictionary.creationTime,
)
)
}
)
}
}
}
}
}
}
#Composable
fun CustomDialogUI(
openDialogCustom: MutableState<Boolean>,
deleteDicState : MutableState<Boolean>
) {
Card(
//shape = MaterialTheme.shapes.medium,
shape = RoundedCornerShape(10.dp),
// modifier = modifier.size(280.dp, 240.dp)
modifier = Modifier.padding(10.dp, 5.dp, 10.dp, 10.dp),
elevation = 8.dp
) {
Column(
modifier = Modifier
.background(Color.White)
) {
//.......................................................................
Image(
painter = painterResource(id = R.drawable.ic_baseline_warning),
contentDescription = null, // decorative
/*contentScale = ContentScale.Fit,
colorFilter = ColorFilter.tint(
color = bar
),*/
modifier = Modifier
.padding(top = 35.dp)
.height(70.dp)
.fillMaxWidth(),
)
Column(modifier = Modifier.padding(16.dp)) {
androidx.compose.material3.Text(
text = "Warning !",
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 5.dp)
.fillMaxWidth(),
style = MaterialTheme.typography.body2,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
androidx.compose.material3.Text(
text = "Are you sure that your previously created dictionary will be deleted?",
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 10.dp, start = 25.dp, end = 25.dp)
.fillMaxWidth(),
)
}
//.......................................................................
Row(
Modifier
.fillMaxWidth()
.padding(top = 10.dp)
.background(bar),
horizontalArrangement = Arrangement.SpaceAround
) {
TextButton(onClick = {
openDialogCustom.value = false
}) {
Text(
"Not Now",
fontWeight = FontWeight.Bold,
color = Color.Black,
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
)
}
TextButton(onClick = {
openDialogCustom.value = false
deleteDicState.value = true
}) {
Text(
"Allow",
fontWeight = FontWeight.ExtraBold,
color = Color.Black,
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
)
}
}
}
}
}
I cannot call the CustomDialogUI in onDeleteClick . If I call it, it gives the following error #Composable invocations can only happen from the context of a #Composable function.
for example like this
CreateYourOwnDictionaryScreen
onDeleteClick = {
Dialog(onDismissRequest = { showAlertDialog.value = false }) {
CustomDialogUI(openDialogCustom = showAlertDialog,deleteDicState)
}
....
I cannot call like this.
So I call it outside of onDeleteClick. or directly in CustomDialogUI if the user presses the delete button, I cannot delete it there because I can't access viewmodel and dictionary there
for example like this
CustomDialogUI
TextButton(onClick = {
openDialogCustom.value = false
viewModel.onEvent(
CreateYourOwnDictionaryEvents.DeleteDictionary(dictionary)
)
scope.launch {
val result = scaffoldState.snackbarHostState.showSnackbar(
message = "dictionary is deleted",
/*actionLabel = "Undo",*/
duration = SnackbarDuration.Short
)
}
}) {
Text(
"Allow",
fontWeight = FontWeight.ExtraBold,
color = Color.Black,
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
)
}
}
I cannot call like this.

Passing MutableStates as composable parameters is considered a bad practice, you should pass raw values and callbacks instead. In your case, you can implement it like this:
#Composable
fun CreateYourOwnDictionaryScreen() {
val showDeleteDialogForItem = remember { mutableStateOf<DictionaryItem?>(null) }
showDeleteDialogForItem.value?.let { itemToDelete ->
DeleteDialog(
onDeleteConfirm = {
viewModel.onEvent()
showDeleteDialogForItem.value = null
},
onCancel = { showDeleteDialogForItem.value = null },
)
}
...
items.forEach { item ->
CreateYourOwnDictionaryItem(
onDeleteClick = { showDeleteDialogForItem.value = item }
)
}
}
#Composable
fun DeleteDialog(
onDeleteConfirm: () -> Unit,
onCancel: () -> Unit,
) {
...
Button(onClick = onCancel) { Text("Cancel") }
Button(onClick = onDeleteConfirm) { Text("Delete") }
}

Related

How to handle displaying and hiding a custom reusable snack bar in jetpack compose?

I have a composable of snackbar. Below is the code
#Composable
fun CealSnackbar(title: String,performAction: () -> Unit,) {
val counterState = remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
while(true) {
delay(4000)
counterState.value = false
performAction()
}
}
AnimatedVisibility(
visible = counterState.value,
enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut(),
) {
Card(
modifier = Modifier
.absoluteOffset(x = 0.dp, y = 10.dp)
.padding(horizontal = 20.dp)
.fillMaxWidth()
.statusBarsPadding()
.background(
color = MaterialTheme.colors.primaryVariant,
shape = RoundedCornerShape(10.dp)
), elevation = 20.dp
) {
Row(
modifier = Modifier
.background(color = MaterialTheme.colors.primaryVariant)
.padding(12.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.error_circle),
contentDescription = title
)
Text(
text = title,
fontFamily = FontFamily(Font(R.font.inter_medium)),
fontSize = 12.sp,
color = MaterialTheme.colors.primary,
modifier = Modifier.padding(horizontal = 10.dp)
)
}
}
}
}
I want the animation when the snackbar appears and disappears and it should be usable in any component
So while using this in other composable I am trying to display it when there is an error msg, the error msg is a state flow
if(errorMsg.isNotEmpty()){
CealSnackbar(
title = errorMsg, performAction = {
registrationViewModel.setErrorMsg("")
}
)
}
If I use it like above the animation does not take place, how should I refactor it so that I use it any composable when there is an error msg
Use SnackbarHost
val snackState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
SnackbarHost(
hostState = snackState
){
CealSnackbar(
title = errorMsg, performAction = {
registrationViewModel.setErrorMsg("")
}
)
}

Nested Column doesn't Recompose

I have nested column, when I click add button the goal is add another text field and when I click delete button (which still hidden because first index) the goal is remove its text field. It seems doesn't recompose but the list size is changed.
I have tried using LazyColumn and foreach inside leads to force close, still no luck.
Any help appreciated, thank you!
My current code :
#Composable
fun ProblemScreen() {
val list = remember {
mutableStateListOf<MutableList<String>>()
}
LaunchedEffect(key1 = Unit, block = {
repeat(3) {
val listDesc = mutableListOf<String>()
repeat(1) {
listDesc.add("")
}
list.add(listDesc)
}
})
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background)
) {
list.forEachIndexed { indexParent, parent ->
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "Parent ${indexParent + 1}", fontSize = 18.sp)
Spacer(modifier = Modifier.weight(1f))
Button(onClick = {
parent.add("")
println("PARENT SIZE : ${parent.size}")
}) {
Icon(imageVector = Icons.Rounded.Add, contentDescription = "Add")
}
}
parent.forEachIndexed { indexChild, child ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
TextField(
value = "",
onValueChange = {
},
colors = TextFieldDefaults.textFieldColors(),
maxLines = 1,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(16.dp))
Button(
onClick = {
parent.removeAt(indexChild)
},
modifier = Modifier.alpha(if (indexChild != 0) 1f else 0f)
) {
Icon(
imageVector = Icons.Rounded.Delete,
contentDescription = "Delete"
)
}
}
}
}
}
}
}
As said in docs, mutable objects that are not observable, such as mutableListOf(), are not observable by Compose and don't trigger a recomposition.
So instead of
val list = remember {
mutableStateListOf<MutableList<String>>()
}
Use:
val list = remember {
mutableStateListOf<List<String>>()
}
And when you need to update the List, create a new one:
//parent.add("")
list[indexParent] = parent + ""

Update LazyColumn after remove opertation in KMM JetpackCompose app

I was deveoping an Kotlin Multiplatform app, where I develop the UI using Jetpack Compose framework, where I recover some data from network using graphql, to keep it into a local database build with SQLDelight.
My poblem comes when I try to implement a remove operation into my localDatabase, due to the LazyColumn component doesn't update after remove some item.
I saw there are some solution using SnapshotStateList, but I don't see how to implemented, and I try to do it using a mutableState of a boolean variable, which tells me if there some change which need to be update, but the Composable doesn't recompose neither.
this is my main component where I added the list:
#Composable
fun MyCharactersView(viewModel: MainViewModel, bottomBar: #Composable () -> Unit, popBack: () -> Unit, navController: NavController, characterSelect: (character: CharacterModel) -> Unit) {
val isLoading = viewModel.isLoading.collectAsState()
val loadError = viewModel.loadError.collectAsState()
val charactersBought = viewModel.characterBought.collectAsState()
val charactersBoughtSnapShot : SnapshotStateList<List<CharacterModel>> = SnapshotStateList(charactersBought.value)
LaunchedEffect(key1 = Unit){
viewModel.getAllBoughtCharacters().also {
Log.d("info", "Lanza el getAllBoughtCharacters:count: ${charactersBought.value.size}")
}
}
when {
isLoading.value -> {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
CircularProgressIndicator(
color = MaterialTheme.colors.primary,
modifier = Modifier
.fillMaxWidth(0.4f)
.fillMaxHeight(0.4f)
.align(Alignment.Center)
)
}
}
loadError.value != "" -> {
Text(
text = "Se ha producido un error",
color = Color.Red,
textAlign = TextAlign.Center
)
}
else -> {
if(viewModel.updateList.value){
viewModel.getAllBoughtCharacters().also {
Log.d("info", "Llama al viewModel.updateList.value: ${charactersBought.value}")
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(
text = "MyCharacters",
style = TextStyle(
color = Color.White,
fontStyle = FontStyle.Italic,
textAlign = TextAlign.Center
)
)},
navigationIcon = {
IconButton(onClick = { popBack() }) {
Icon(Icons.Filled.ArrowBack, contentDescription = "Back")
}
}
)
},
bottomBar = bottomBar)
{ paddingValues ->
Surface(
modifier = Modifier
.background(
Color.Transparent
)
) {
if(charactersBought.value.isEmpty()){
Text(
text = "You don't have any character jet.\nTry to buy some one",
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
modifier = Modifier.padding(start = (LocalConfiguration.current.screenWidthDp/4).dp, end = 0.dp, top = (LocalConfiguration.current.screenHeightDp/4).dp)
)
}else{
LazyColumn(
contentPadding = paddingValues,
modifier = Modifier
.background(Color.Transparent)
) {
Log.d("info", "CharacterResponse in items function: ${charactersBought.value}")
items(charactersBought.value) { character ->
MyCharactersListRowView(
navController = navController,
viewModel = viewModel,
characterModel = character!!,
characterSelected = characterSelect
)
}
}
}
}
}
}
}
}
And this is my ListRow component:
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun MyCharactersListRowView(
navController: NavController,
viewModel: MainViewModel,
characterModel: CharacterModel,
characterSelected: (character : CharacterModel) -> Unit
) {
val openDialog = remember { mutableStateOf(false) }
if(openDialog.value) {
SimpleAlertDialog(
show = true,
onConfirm = { viewModel.deleteCharacter(characterModel.id.toLong().also {
if(viewModel.rowAffected.value.toInt() != 0) {
Toast.makeText(appContext, "${characterModel.name} remove correctly !", Toast.LENGTH_LONG).show().also {
openDialog.value = false
viewModel.updateList.value = true
}
}
}) },
onDismiss = { openDialog.value = false },
textDescription = "Would you like to remove ${characterModel.name} from your characters?",
textTittle = "Remove"
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = { characterSelected(characterModel) })
.padding(vertical = 8.dp, horizontal = 16.dp)
.combinedClickable(
onLongClick = {
openDialog.value = true
},
onClick = {
navController.navigate(Screens.CharacterDetailsScreen.route + "/${characterModel.id}")
}
),
verticalAlignment = Alignment.CenterVertically
){
AsyncImage(
model = characterModel.image,
contentDescription = characterModel.name,
contentScale = ContentScale.Fit,
modifier = Modifier
.size(60.dp)
.clip(CircleShape)
)
Column(modifier = Modifier.weight(1F)) {
Text(
text = characterModel.name,
style = MaterialTheme.typography.h5,
fontWeight = FontWeight.Bold
)
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
Text(
text = "Seen in ${characterModel.numberEpisodes} episodes, most frequently in ${characterModel.location}",
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Thin
)
}
}
}
Divider()
}
So if you knows some way to force to recompose a composable, (which I think is not possible), or a working solution using the SnapShotStateList, and can share, take thanks in advance !

compile time error: #Composable invocations can only happen from the context of a #Composable function

I have a compile time error as shown in the title
(#Composable invocations can only happen from the context of a #Composable function),
I have one card in this code so I want to show An AlertDialog after clicking the card clickable,
I know that this problem is exist in the platform,
So how can i solve this problem?
#Composable
fun Screen_A_with_WithTopBar(navController: NavController) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "Top App Bar")
},
navigationIcon = {
IconButton(onClick = {
navController.navigate(Screen.Home.route)
}) {
Icon(Icons.Filled.Menu, "backIcon")
}
},
backgroundColor = MaterialTheme.colors.primary,
contentColor = Color.White,
elevation = 10.dp
)
}, content = {
Screen_A(navController)
}
)
}
#Composable
fun Screen_A(navController: NavController) {
val context = LocalContext.current
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
//.background(ScreenColor)
) {
Text(
text = "Dooro",
fontSize = 42.sp
)
Spacer(modifier = Modifier.height(33.dp))
Row(
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.SpaceAround,
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
) {
Card(
modifier = Modifier
.width(150.dp)
.height(80.dp)
.clickable {
Alert()
},
RoundedCornerShape(7.dp),
elevation = 7.dp
//.padding(40.dp)
) {
Text(
text = "None",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
}
} // END Row
}
}
#Composable
fun Alert() {
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")
}
}
}
)
}
}
Here you can use mutableStateOf to be able to show your Alert.
on Screen_A, you should add the following
var showAlert by mutableStateOf(false)
#Composable
private fun ShowAlert() {
if (showAlert)
Alert()
}
Finally in your Card, you will change the value of showAlert field to true.
Card(
modifier = Modifier
.width(150.dp)
.height(80.dp)
.clickable {
showAlert = true
},
RoundedCornerShape(7.dp),
elevation = 7.dp
//.padding(40.dp)
)

ModalDrawer requires coroutine context to change state hide and show jetpack compose

I need to use a coroutine context to work with modalState.show() or modalState.hide() in my code
#ExperimentalMaterialApi
#Composable
fun ModalBottomSheetLayoutDemo() {
val modalState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
Button(modifier = Modifier.padding(16.dp), onClick = { modalState.show() }) {
Text("Show Bottom Sheet")
}
ModalBottomSheetLayout(sheetState = modalState, sheetContent = {
Text(
modifier = Modifier.padding(start = 8.dp, top = 8.dp),
text = "Title",
fontWeight = FontWeight.Bold,
style = typography.h5
)
Text(
modifier = Modifier.padding(start = 8.dp, top = 8.dp),
text = "Content example right here :)",
style = typography.body1
)
Row(modifier = Modifier.align(Alignment.CenterHorizontally).padding(8.dp)) {
Button(modifier = Modifier.padding(end = 8.dp), onClick = { modalState.hide() }) {
Text("Cancel")
}
Button(onClick = { modalState.hide() }) {
Text("Ok")
}
}
}, sheetElevation = 8.dp) {}
}
Before it was working and now it requires a coroutine context, how to execute the context within jetpack compose ?
Call rememberCoroutineScope(), hold onto it in a variable, and use that to launch() your show() and hide() calls:
#ExperimentalMaterialApi
#Composable
fun ModalBottomSheetLayoutDemo() {
val modalState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val sillyScope = rememberCoroutineScope()
Button(modifier = Modifier.padding(16.dp), onClick = { sillyScope.launch { modalState.show() } }) {
Text("Show Bottom Sheet")
}
ModalBottomSheetLayout(sheetState = modalState, sheetContent = {
Text(
modifier = Modifier.padding(start = 8.dp, top = 8.dp),
text = "Title",
fontWeight = FontWeight.Bold,
style = typography.h5
)
Text(
modifier = Modifier.padding(start = 8.dp, top = 8.dp),
text = "Content example right here :)",
style = typography.body1
)
Row(modifier = Modifier.align(Alignment.CenterHorizontally).padding(8.dp)) {
Button(modifier = Modifier.padding(end = 8.dp), onClick = { modalState.hide() }) {
Text("Cancel")
}
Button(onClick = { sillyScope.launch { modalState.hide() } }) {
Text("Ok")
}
}
}, sheetElevation = 8.dp) {}
}
AFAICT, the suspend call is used because the animation APIs in Compose UI are using coroutines, so we need to apply a suitable CoroutineScope. rememberCoroutineScope() will give you a scope that is good for as long as this branch of your composition is around in some form.

Categories

Resources