Sitemap
Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.

Create custom & reusable alert dialog that can be used in any Composable.

7 min readFeb 6, 2024

--

I’v been working on a project recently and I’m implementing for both iOS and Android, and so when I create a custom view in SwiftUI, I try to implement the same look and feel in Jetpack compose. one of those views is the CustomAlert.

If you want to read the SwiftUI version, check out this article 👇🏻:

In this article we will explore how we can build our own customized and reusable CustomAlert composable.

Final output should look like the following 📲

Press enter or click to view image in full size
Press enter or click to view image in full size

Feel free to download starter project here, if you want to follow along — it is just a simple MainScreen() composable to test our custom alert — , or make the changes in your own project.

TL;TR;

If you don’t want the explanation part, feel free to jump to the end of the article, you will find the full source code, download or copy CustomAlert.kt and CustomAlertDialog.kt files and customize CustomAlert to match your app’s look and feel, or even add your ideas to match your need.

Let’s Start 👩🏻‍💻:

❶ Create new Kotlin file named CustomAlert, create a generic @Composable function that takes the following parameters:

  1. title: String that describes the title of the alert.
  2. message: String that describes the message for the alert.
  3. actionText: String that describes the text of action button.
  4. data: An optional value of generic type T, this data will populate the fields of an alert that will be displayed to the user.
  5. showAlert: MutableState of Boolean value that determines whether to present the alert.
  6. actionWithValue: The alert’s action given the currently available data.
  7. action: The alert’s action without data.
@Composable
fun <T> CustomAlert(
// 1.
title: String,
// 2.
message: String,
// 3.
actionText: String,
// 4.
data: T?,
// 5.
showAlert: MutableState<Boolean>,
// 6.
actionWithValue: ((T) -> Unit)?,
// 7.
action: (() -> Unit)?,
) {
// TODO: Alert
}

In case data is not null, then we will return actionWithValue, otherwise, we will return action — without a value.

❷ Next, create new Kotlin file named CustomAlertDialog, create two @Composable functions, that will wrap CustomAlert in a Dialog.

𝟷 The first function will be generic, and will have data parameter of type T, and the action parameter of type (T) -> Unit, note that action here is null.

𝟸 The second function will not have data, and action will be of type () -> Unit, note that data and actionWithValue are both null here.

@Composable
fun <T> CustomAlertDialog(
title: String,
message: String,
actionText: String,
data: T?,
showAlert: MutableState<Boolean>,
action: (T) -> Unit
) {
Dialog(
onDismissRequest = { showAlert.value = false },
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
CustomAlert(
title = title, message = message, actionText = actionText,
data = data, showAlert = showAlert,
actionWithValue = action, action = null
)
}
}

@Composable
fun CustomAlertDialog(
title: String,
message: String,
actionText: String,
showAlert: MutableState<Boolean>,
action: () -> Unit
) {
Dialog(
onDismissRequest = { showAlert.value = false },
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
CustomAlert(
title = title, message = message, actionText = actionText,
data = null, showAlert = showAlert,
actionWithValue = null, action = action
)
}
}

Let’s continue with the UI

❸ Inside CustomAlert composable, add the following:

Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Card(
modifier = Modifier.padding(8.dp),
shape = RoundedCornerShape(35.dp)
) {
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// TODO: components
}
}
}

Inside second Column, add the following:

/// Title
Text(
title,
modifier = Modifier.fillMaxWidth(),
color = AlertCyan, //--> Color(0xFF47B0EC)
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center
)

/// Message
Text(
message,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
fontSize = 19.sp,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge
)

// TODO: Add Buttons...

Now, replace // TODO: Add Buttons… with the following Row containing cancel and action buttons:

In onClick of action button, if actionWithValue is not null, and we have data, return actionWithValue(data), otherwise return action().

Row(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Cancel button
Button(
onClick = { showAlert.value = false },
modifier = Modifier
.background(
MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(35.dp)
)
.height(55.dp)
.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
) {
Text(
text = "Cancel", color = AlertCyan,
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.titleMedium
)
}

// Action button
Button(
onClick = {
showAlert.value = false

/// Action...
if (actionWithValue != null) {
data?.let {
actionWithValue(data)
}
} else if (action != null) {
action()
}
},
modifier = Modifier
.background(
AlertCyan,
shape = RoundedCornerShape(35.dp)
)
.height(55.dp)
.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
),
) {
Text(
text = actionText,
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.titleMedium
)
}
}

Note: I’m using data?.let { } here, to guarantee returned data is T not T?, and if passed data is null, action block will not be performed.

Now, the UI is ready…

Get Marwa Diab’s stories in your inbox

Join Medium for free to get updates from this writer.

❹ Go to MainScreen composable and replace // TODO: Show Book Alert with the following:

CustomAlertDialog(
"Random Book",
"You have selected ${randomBook?.title} by ${randomBook?.author}",
"Yes, Done",
data = randomBook,
showAlert = showBookAlert,
) {
selectedBook.value = it
}

replace // TODO: Show Credits Alert with the following:

CustomAlertDialog(
"Credits",
"This custom alert tutorial was implemented by Marwa",
"OK",
showAlert = showCreditsAlert
) {
// Some action here…
}

Run code 📲

Let’s add some animation ✨

❺ In CustomAlert add the following:

  1. Create animateState variable of type MutableTransitionState with false initial state.
  2. set targetState of animationState with showAlert value, and in order to run this only once when the composition starts, put it inside LaunchEffect(Unit).
@Composable
fun <T> CustomAlert(
// parameters…
) {
// 1.
val animationState = remember { MutableTransitionState(false) }

// Run only once
LaunchedEffect(Unit) {
// 2.
animationState.targetState = showAlert.value
}

// Column content…
}

Next, wrap Card with AnimatedVisibility, define slideInHorizontally for enter transition, and slideOutHorizontally for exit transition — feel free to choose whichever transition animation you like:

AnimatedVisibility(
animationState,
enter = slideInHorizontally(
animationSpec = tween(200)
),
exit = slideOutHorizontally(
animationSpec = tween(200)
)
) {
// Card content…
}

Inside onClick of both buttons, add the following before showAlert.value = false:

animationState.targetState = false

Run code 📲

As you can see alert is presented using the slideInHorizontally enter transition, but when alert is being dismissed, the slideOutHorizontally exit transition did not work.. that’s because we dismissed the alert before animation finishes by setting showAlert value to false.

To solve that first remove showAlert.value = false from onClick in both buttons, then after AnimatedVisibility, add the following:

  1. CurrentState changes after animation finishes.
  2. Animation is completed when currentState is the same as targetState, and currentState is false.
  3. Set showAlert to false to dismiss.
// 1.
LaunchedEffect(animationState.currentState) {
// 2.
if (!animationState.currentState && animationState.targetState == animationState.currentState) {
// 3.
showAlert.value = false
}
}

Run code 📲

Now the exit transition work correctly, but we want it to slide out to the other side, to do so add targetOffsetX in the exit transition like follows:

exit = slideOutHorizontally(
animationSpec = tween(200),
targetOffsetX = { fullWidth -> fullWidth }
)

Note: animation transition seems sluggish on the Emulator, but it should look good on a real device.

Run code 📲

And Voilà! 🎉

With that we’ve created custom, generic & reusable alert dialog, with animation and transition.
Feel free to add any ideas and changes to make it custom to your needs.

You can find the project’s full source code on GitHub 🚀
If you liked this, don’t forget to 👏 and follow 😍.
Feedbacks are most welcome, drop them below in the comments section.

“ Everyone has something to learn. Everyone has something to teach.” ~ Paul Hudson

Stackademic 🎓

Thank you for reading until the end. Before you go:

--

--

Marwa Diab
Marwa Diab

Written by Marwa Diab

Passionate indie iOS/Android developer 👩🏻‍💻

Stackademic
Stackademic

Published in Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.