LinearProgressIndicator and CircularProgressIndicator in Android Studio | Jetpack Compose

Video thumbnail

In this post, we will talk about another method to indicate the loading progress of an operation that can replace the now deprecated ProgressDialog in newer versions of Android; this means that in future versions of Android, this software component will cease to exist.

The UI element in question looks like this:

YouTube progressBar

And it is known as ProgressBar; as you can see, it is a kind of animated side bar which has already been present on websites like YouTube or in the YouTube Android app itself among other applications, and in modern Android, we have several variants such as the Circular or Linear types.

We left off knowing how to use Dropdown option menus in Android Studio.

Progress Indicators: Linear and Circular with Jetpack Compose

With the arrival of Jetpack Compose, the way to build user interfaces in Android has changed to a declarative approach. Instead of XML, we now use Composable functions written in Kotlin.

In Compose, progress indicators are called CircularProgressIndicator (for the circle style) and LinearProgressIndicator (for the horizontal bar style).

LinearProgressIndicator and CircularProgressIndicator

1. Circular Progress Indicator: Indeterminate Progress

Used when the time an operation will take is unknown.

@Composable
fun MyLoadingScreen() {
    Box(
        contentAlignment = Alignment.Center,
        modifier = Modifier.fillMaxSize()
    ) {
        CircularProgressIndicator()
    }
}

Circular Progress Indicator Legacy

It works identically to the previous one, but with a circular shape. Although you can pass the value directly, using braces {} is recommended to optimize recompositions by using lambda functions.

CircularProgressIndicator(
   progress = { currentProgress }
)
CircularProgressIndicator(progress = currentProgress) // legacy

2. Linear Progress Indicator: Determinate Progress

This is the one that occupies the width of the screen. We can customize its size and thickness using a Modifier.

To show or hide it, you can use a simple condition in your Composable:

@Composable
fun MyLoadingScreen() {
    Box(
        contentAlignment = Alignment.Center,
        modifier = Modifier.fillMaxSize()
    ) {
        LinearProgressIndicator(progress = { animatedProgress },
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 32.dp))
    }
}

It is used to show the progress of a task with a known percentage (from 0.0 to 1.0).

CircularProgressIndicator and LinearProgressIndicator

An example where you can see the functioning of both progress composables:

@Composable
fun MyDownloadProgress() {
    var progress by remember { mutableFloatStateOf(0.5f) }
    val animatedProgress by animateFloatAsState(
        targetValue = progress,
        animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
    )

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        // Indicador lineal
        LinearProgressIndicator(progress = { animatedProgress },
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 32.dp))

        Spacer(Modifier.height(16.dp))

        // Indicador circular
        CircularProgressIndicator(progress = { animatedProgress }) // Lambda
        // CircularProgressIndicator(progress =  animatedProgress ) // deprecated
    }
}

In this example, you would update the progress variable as your task advances.

Customization and Visual Details

By default, these components work with values between 0 and 1. For example, a value of 0.5 will fill the bar exactly halfway.

If you hover over the component and use Control + Click, you will see all the properties you can change:

  • Color: The color of the active bar.
  • Track Color: The background color of the bar (the track).
  • Stroke Width: The thickness of the stroke.

An interesting detail is that, depending on whether you use the lambda version or the standard version, the visual finish may vary slightly (such as the rounding of the ends or the color of the gray track).

Legacy Form (XML View System)

This is the traditional way to add progress indicators in Android, using XML layout files.

Ways to use the ProgressBar

ProgressBar can be used to indicate the progress of a task in percentages (0% to 100%) or in an indeterminate way as shown in the previous image.

You simply need to set it to be visible during loading times and hide it (invisible) when the content is ready; as we can see, this UI element has the drawback that it does not allow blocking the entire activity or fragment that executes it to prevent the user from interacting with the interface, but as a best practice, such blocking should be done manually by hiding or disabling the interface elements we consider necessary.

Now let's see the options we have for using ProgressBar:

1. Determinate ProgressBar

To show the progress of the ProgressBar in terms of percentages, we can use the android:progress attribute; we can also vary the percentage levels to define a minimum and maximum boundary with the android:min and android:max attributes respectively:

 <ProgressBar
      android:id="@+id/determinateBar"
      style="@android:style/Widget.ProgressBar.Horizontal"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:progress="25"/ >

The code above shows how to create a ProgressBar with determinate progress and a default value set to 25%, which of course can be varied programmatically with the setProgress(int) method.

Of course, programmatic methods also exist to define the minimum and maximum boundaries:

setMax(int max) setMin(int min)

2. Indeterminate ProgressBar

For the ProgressBar, we simply need to use it without the attributes seen above; that is, without android:progress, android:min, and android:max; the code results as follows:

<ProgressBar
      android:id="@+id/indeterminateBar"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      />

You can consult the full documentation at the following link: Android Developer ProgressBar.

Practical Case

In either of the two cases we explained, the idea is that upon finishing the processing, data retrieval, or any other process that requires showing a loading element, you hide the ProgressBar; the following practical example shows a case using the Retrofit library, which is a topic for a later post; in general, we show the ProgressBar when we are obtaining data from the server and hide it once the server gives us a response:

public class FeeActivity extends AppCompatActivity {
    private ActionBarDrawerToggle mDrawerToggle;
    private static final String TAG = FeeActivity.class.getSimpleName();
    private RecyclerView rvNotificaciones;
    private RelativeLayout rlSinFees;
    private ProgressBar progressBar;
    private ArrayList<Fee> fees;
    private int loanId;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fee);
        loanId = getIntent().getExtras().getInt("loan_id");
        progressBar = (ProgressBar) findViewById(R.id.progressbar);
        rvNotificaciones = (RecyclerView) findViewById(R.id.rv_fees);
        rlSinFees = (RelativeLayout) findViewById(R.id.rl_sin_fees);
        init();
    }
    @Override
    protected void onResume() {
        super.onResume();
    }
    @Override
    protected void onPause() {
        super.onPause();
    }
    public void init() {
        fees = new ArrayList<>();
        generarGridLayout();
        cargarCuota();
    }
    public void generarGridLayout() {
        LinearLayoutManager llm = new LinearLayoutManager(this);
        rvNotificaciones.setLayoutManager(llm);
        rvNotificaciones.setNestedScrollingEnabled(false);
    }
    public void initAdapter(CuotaAdapter cuotaAdapter) {
        rvNotificaciones.setAdapter(cuotaAdapter);
    }
    public void cargarCuota() {
        RestApiAdapter restApiAdapter = new RestApiAdapter();
            Gson gson = restApiAdapter.convierteGsonDesearilizadorCuotas();
            EndpointsApi endpointsApi = restApiAdapter.establecerConexionRestApi(gson);
            Call<FeeResponse> responseCall = endpointsApi.getCuotasDia(UserPreferences.getUsuarioId(FeeActivity.this));
            responseCall.enqueue(new Callback<FeeResponse>() {
                @Override
                public void onResponse(Call<FeeResponse> call, Response<FeeResponse> response) {
                    FeeResponse feeResponse = response.body();
                    if (feeResponse != NULL) {
                        fees = feeResponse.getFees();
                    }
                    if (fees.size() == 0) {
                        rvNotificaciones.setVisibility(View.GONE);
                        rlSinFees.setVisibility(View.VISIBLE);
                    }
                    initAdapter(new CuotaAdapter(fees, FeeActivity.this));
                    progressBar.setVisibility(View.INVISIBLE);
                }
                @Override
                public void onFailure(Call<FeeResponse> call, Throwable t) {
                    Log.e("Error", t.toString());
                    Toast.makeText(FeeActivity.this, getResources().getString(R.string.error_login_local_titulo), Toast.LENGTH_LONG).show();
                    progressBar.setVisibility(View.INVISIBLE);
                }
            });
    }
}

You can consult the full documentation at the following links:

Next step, learn how to use MediaPlayer to play audio in Android.

Learn how to implement CircularProgressIndicator/LinearProgressIndicator in Jetpack Compose. Step-by-step guide to handling mutable states, customizing colors, and optimizing performance with lambdas.

I agree to receive announcements of interest about this Blog.

Andrés Cruz

ES En español