
In this article, we’ll learn how to create the following loading dialog using Android Jetpack Compose:

Prerequisites:
Let’s get started.
First, create an empty Compose project and open the MainActivity.kt. Add the following code:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
YourProjectNameTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
LoadingDialog()
}
}
}
}
}
}
// dialog
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun LoadingDialog(
cornerRadius: Dp = 16.dp,
progressIndicatorColor: Color = Color(0xFF35898f),
progressIndicatorSize: Dp = 80.dp
) {
}
// progress indicator
@Composable
private fun ProgressIndicatorLoading(
progressIndicatorSize: Dp,
progressIndicatorColor: Color
) {
}
// view model to manage the state
class MyViewModel : ViewModel() {
}
If you face any problem with imports, look at the gradle files used in the project.
We created two methods and a class:
LoadingDialog() – It contains the code for the AlertDialog.
ProgressIndicatorLoading() – We add the progress indicator here.
MyViewModel – We manage the state here.
First, let’s implement the view model.
class MyViewModel : ViewModel() {
// if the dialog is open or not
var open = MutableLiveData<Boolean>()
fun startThread() {
viewModelScope.launch {
withContext(Dispatchers.Default) {
// Do the background work here
// I'm adding the delay
delay(3000)
}
closeDialog()
}
}
private fun closeDialog() {
open.value = false
}
}
We created a boolean variable called open. It represents if the dialog is open or not.
Next, we added the startThread() function. We call it right after the dialog is opened. It launches a coroutine to perform the background tasks. When the tasks are completed, we are closing the dialog.
Let’s implement the progress indicator.
@Composable
private fun ProgressIndicatorLoading(
progressIndicatorSize: Dp,
progressIndicatorColor: Color
) {
val infiniteTransition = rememberInfiniteTransition()
val angle by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 600 // animation duration
}
)
)
CircularProgressIndicator(
progress = 1f,
modifier = Modifier
.size(progressIndicatorSize)
.rotate(angle)
.border(
12.dp,
brush = Brush.sweepGradient(
listOf(
Color.White, // add background color first
progressIndicatorColor.copy(alpha = 0.1f),
progressIndicatorColor
)
),
shape = CircleShape
),
strokeWidth = 1.dp,
color = Color.White // Set background color
)
}
The idea is to create a CircularProgressIndicator() and rotate it using the infinite transition API. To get the loading effect, we added a gradient background to it.
Let’s see how it looks. Call it from the LoadingDialog() method.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun LoadingDialog(
cornerRadius: Dp = 16.dp,
progressIndicatorColor: Color = Color(0xFF35898f),
progressIndicatorSize: Dp = 80.dp
) {
ProgressIndicatorLoading(
progressIndicatorSize = progressIndicatorSize,
progressIndicatorColor = progressIndicatorColor
)
}
Output:

Next, in the LoadingDialog() method, remove the ProgressIndicatorLoading() and add the AlertDialog.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun LoadingDialog(
cornerRadius: Dp = 16.dp,
progressIndicatorColor: Color = Color(0xFF35898f),
progressIndicatorSize: Dp = 80.dp
) {
AlertDialog(
onDismissRequest = {
},
properties = DialogProperties(
usePlatformDefaultWidth = false // disable the default size so that we can customize it
)
) {
Column(
modifier = Modifier
.padding(start = 42.dp, end = 42.dp) // margin
.background(color = Color.White, shape = RoundedCornerShape(cornerRadius))
.padding(top = 36.dp, bottom = 36.dp), // inner padding
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
ProgressIndicatorLoading(
progressIndicatorSize = progressIndicatorSize,
progressIndicatorColor = progressIndicatorColor
)
// Gap between progress indicator and text
Spacer(modifier = Modifier.height(32.dp))
// Please wait text
Text(
text = "Please wait...",
style = TextStyle(
color = Color.Black,
fontSize = 16.sp,
fontWeight = FontWeight.Normal
)
)
}
}
}
Output:

Inside the dialog, we added a Column layout and set the margin, shape, and inner padding. Next, we added the progress indicator, text, and Spacer() for the gap.
Now, it’s time to create a button to open the dialog manually.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun LoadingDialog(
cornerRadius: Dp = 16.dp,
progressIndicatorColor: Color = Color(0xFF35898f),
progressIndicatorSize: Dp = 80.dp
) {
val viewModel: MyViewModel = viewModel()
val showDialog by viewModel.open.observeAsState(initial = false) // initially, don't show the dialog
if (showDialog) {
AlertDialog(
onDismissRequest = {
},
properties = DialogProperties(
usePlatformDefaultWidth = false // disable the default size so that we can customize it
)
) {
Column(
modifier = Modifier
.padding(start = 42.dp, end = 42.dp) // margin
.background(color = Color.White, shape = RoundedCornerShape(cornerRadius))
.padding(top = 36.dp, bottom = 36.dp), // inner padding
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
ProgressIndicatorLoading(
progressIndicatorSize = progressIndicatorSize,
progressIndicatorColor = progressIndicatorColor
)
// Gap between progress indicator and text
Spacer(modifier = Modifier.height(32.dp))
// Please wait text
Text(
text = "Please wait...",
style = TextStyle(
color = Color.Black,
fontSize = 16.sp,
fontWeight = FontWeight.Normal
)
)
}
}
}
Button(
onClick = {
viewModel.open.value = true
viewModel.startThread()
}
) {
Text(text = "Show Loading Dialog")
}
}
Output:

We are managing the state using the ViewModel. The showDialog variable observes the value of the open. Initially, it is false. That is why the dialog is not visible.
Next, we put the whole AlertDialog inside the if condition. If the showDialog is true, the dialog will be visible.
Inside the button onClick, we made the open variable true and called the startThread() method. As the open becomes true, the showDialog becomes true and the dialog will be rendered on the screen. Inside the startThread() method, we added a delay of 3 seconds. After 3 seconds, the open becomes false, as a result, the showDialog becomes false, and the dialog will be removed from the screen.
Related: How to Disable Ripple Effect in Jetpack Compose?
Here is the complete code:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
YourProjectNameTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
LoadingDialog()
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun LoadingDialog(
cornerRadius: Dp = 16.dp,
progressIndicatorColor: Color = Color(0xFF35898f),
progressIndicatorSize: Dp = 80.dp
) {
val viewModel: MyViewModel = viewModel()
val showDialog by viewModel.open.observeAsState(initial = false) // initially, don't show the dialog
if (showDialog) {
AlertDialog(
onDismissRequest = {
},
properties = DialogProperties(
usePlatformDefaultWidth = false // disable the default size so that we can customize it
)
) {
Column(
modifier = Modifier
.padding(start = 42.dp, end = 42.dp) // margin
.background(color = Color.White, shape = RoundedCornerShape(cornerRadius))
.padding(top = 36.dp, bottom = 36.dp), // inner padding
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
ProgressIndicatorLoading(
progressIndicatorSize = progressIndicatorSize,
progressIndicatorColor = progressIndicatorColor
)
// Gap between progress indicator and text
Spacer(modifier = Modifier.height(32.dp))
// Please wait text
Text(
text = "Please wait...",
style = TextStyle(
color = Color.Black,
fontSize = 16.sp,
fontWeight = FontWeight.Normal
)
)
}
}
}
Button(
onClick = {
viewModel.open.value = true
viewModel.startThread()
}
) {
Text(text = "Show Loading Dialog")
}
}
@Composable
private fun ProgressIndicatorLoading(
progressIndicatorSize: Dp,
progressIndicatorColor: Color
) {
val infiniteTransition = rememberInfiniteTransition()
val angle by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 600
}
)
)
CircularProgressIndicator(
progress = 1f,
modifier = Modifier
.size(progressIndicatorSize)
.rotate(angle)
.border(
12.dp,
brush = Brush.sweepGradient(
listOf(
Color.White, // add background color first
progressIndicatorColor.copy(alpha = 0.1f),
progressIndicatorColor
)
),
shape = CircleShape
),
strokeWidth = 1.dp,
color = Color.White // Set background color
)
}
class MyViewModel : ViewModel() {
var open = MutableLiveData<Boolean>()
fun startThread() {
viewModelScope.launch {
withContext(Dispatchers.Default) {
// Do the background work here
// I'm adding delay
delay(3000)
}
closeDialog()
}
}
private fun closeDialog() {
open.value = false
}
}
Output:

This is how we create a simple loading dialog in Android Jetpack Compose. I hope you have learned something new. If you have any doubts, leave a comment below.
Related Articles:
References: