Say Goodbye to Plain Toasts! Design Custom Jetpack Compose Toasts
A Toast in Android is a small, temporary pop-up message that appears on the screen to provide quick feedback to the user. It does not block user interaction and disappears automatically after a short duration.
For example, when clicking a mic button, a toast can show:
? "Listening..." when recording starts
? "Stopped listening" when recording stops
However, the default Android Toast feels plain and may not align with your app’s theme. What if you could create a stylish, customizable toast that matches your app’s branding?
Let’s explore a clean and reusable way to implement a custom toast in Jetpack Compose.
1?? Defining the Custom Toast Composable ??
Below is a reusable ToastMessage Composable that listens to state changes from ToastManager and automatically disappears after 2 seconds:
@Composable
fun ToastMessage(
modifier: Modifier = Modifier.padding(12.dp),
shape: RoundedCornerShape = CircleShape
) {
val toastMessage by ToastManager.toastMessage.collectAsState()
val showMessage by ToastManager.showMessage.collectAsState()
val isSuccess by ToastManager.isSuccess.collectAsState()
if (showMessage) {
val backgroundColor = if (isSuccess) {
Color(0xFFB2F1C0)
} else {
Color(0xFFFFDAD6)
}
val iconColor = if (isSuccess) {
Color(0xFF2F6A43)
} else {
Color(0xFFFF2514)
}
val icon = if (isSuccess) Icons.Default.CheckCircle else Icons.Default.Close
val contentDescription = if (isSuccess) "Success icon" else "Error icon"
LaunchedEffect(showMessage) {
if (showMessage) {
delay(2000) // Hide after 2 seconds
ToastManager.hideToast()
}
}
Row(
modifier = modifier
.background(backgroundColor, shape)
.padding(12.dp)
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = iconColor,
modifier = Modifier.padding(end = 8.dp)
)
Text(text = toastMessage)
}
}
}
Why Use ToastManager?
Instead of managing toast visibility inside each screen, we use a centralized ToastManager to handle state updates. This makes our toast easy to trigger from anywhere in the app.
Now, let’s define the ToastManager in the next step.
2??Defining the ToastManager Singleton ??
A singleton ToastManager helps us manage toast messages centrally, ensuring a consistent UI experience across the app. This way, we don’t need to declare a new instance in every ViewModel—we can just call ToastManager.showToast(...) whenever needed.
object ToastManager {
private val _toastMessage = MutableStateFlow("")
val toastMessage: StateFlow<String> = _toastMessage
private val _showMessage = MutableStateFlow(false)
val showMessage: StateFlow<Boolean> = _showMessage
private val _isSuccess = MutableStateFlow(true)
val isSuccess: StateFlow<Boolean> = _isSuccess
fun showToast(message: String, isSuccess: Boolean) {
_toastMessage.update { message }
_isSuccess.update { isSuccess }
_showMessage.update { true }
}
fun hideToast() {
_showMessage.update { false }
_toastMessage.update { "" }
}
}
Why Singleton?
Since ToastManager is a singleton, we don’t need to initialize it in every ViewModel. We can directly call ToastManager.showToast(...) anywhere in the app.
领英推荐
3?? Integrating the Toast in Screen and ViewModel ??
Step 1: Add ToastMessage() to Your Screen
Place ToastMessage() inside the main screen composable. Typically, it appears at the bottom center of the screen.
@Composable
fun MyScreen(viewModel: MyViewModel = hiltViewModel()) {
Scaffold(
topBar = { /* Top bar content here */ },
content = { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// Other screen components
Button(onClick = { viewModel.insertData("New Data") }) {
Text("Add Data")
}
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 124.dp)
) {
ToastMessage()
}
}
}
)
}
Step 2: Trigger Toast in ViewModel
When a user inserts data by clicking a button, the ViewModel triggers a toast message based on success or failure.
@HiltViewModel
class MyViewModel @Inject constructor(private val repository: Repository) : ViewModel() {
fun insertData(data: String) {
viewModelScope.launch {
try {
repository.insertData(data)
ToastManager.showToast(message = "Data added successfully", isSuccess = true)
} catch (e: Exception) {
ToastManager.showToast(message = "Error while entering data", isSuccess = false)
}
}
}
}
?? Final Output
Your custom toast message is now:
Reusable across all screens
Styled to match your app’s branding
Triggerable from anywhere in your app
???Conclusion
With this setup, you can seamlessly integrate a custom toast into your Jetpack Compose app.
Why it’s better than the default Toast:
? Matches app theme – Customizable colors, shapes, and animations
? More control – Centralized ToastManager handles messages efficiently
? Reusable – Works across all screens without ViewModel-specific toast logic
Try it out in your project and enhance your app’s UX!??