Designing Placeholder and Loading States in XML

In modern Android development, providing a seamless user experience is paramount. A critical aspect of this is designing effective placeholder and loading states, especially when dealing with asynchronous data loading. Users should be informed that content is on its way, preventing frustration and improving engagement. While Jetpack Compose offers declarative approaches, many projects still utilize XML for defining UI. This blog post will explore designing placeholder and loading states in XML-based Android layouts effectively.

Why are Placeholder and Loading States Important?

Placeholder and loading states are UI patterns that inform users about the status of their request or content. They serve several crucial purposes:

  • Perceived Performance: Visual feedback helps users perceive the app as faster, even if data loading takes time.
  • Reduced Frustration: Prevents users from wondering if the app is working or frozen.
  • Guidance: Indicates the expected content type and layout.
  • Enhanced User Experience: Improves overall user satisfaction and engagement.

Approaches to Designing Placeholder and Loading States in XML

There are several strategies to implement placeholder and loading states in XML. Here, we’ll discuss the most common and effective approaches:

1. Using ViewStub

ViewStub is a lightweight, invisible view that can be inflated at runtime. It is perfect for including layouts only when needed, such as placeholder or loading views.

Step 1: Define the Placeholder/Loading Layout

Create an XML layout file for your placeholder or loading view. For instance, loading_state.xml might contain:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

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

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Loading..."
        android:textAppearance="?android:attr/textAppearanceMedium"/>

</LinearLayout>
Step 2: Include ViewStub in Your Main Layout

In your main layout (e.g., activity_main.xml), add the ViewStub:

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/contentTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Content Loaded"
        android:visibility="gone"/>

    <ViewStub
        android:id="@+id/loadingViewStub"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:inflatedId="@+id/loadingView"
        android:layout="@layout/loading_state"/>

</RelativeLayout>

Notice that contentTextView is initially hidden.

Step 3: Inflate ViewStub Programmatically

In your Activity or Fragment, handle the inflation and visibility:

import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.ViewStub;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    private View loadingView;
    private TextView contentTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        contentTextView = findViewById(R.id.contentTextView);
        ViewStub stub = findViewById(R.id.loadingViewStub);
        loadingView = stub.inflate(); // Inflate ViewStub

        // Simulate loading data
        new Handler().postDelayed(() -> {
            loadingView.setVisibility(View.GONE);
            contentTextView.setVisibility(View.VISIBLE);
        }, 2000); // Simulate 2 seconds loading
    }
}

This code:

  • Inflates the ViewStub to display the loading view.
  • Simulates data loading with a Handler.
  • Hides the loading view and shows the content when the data is “loaded.”

2. Using setVisibility with Included Layouts

Another approach involves including the placeholder or loading layout directly in your main layout and toggling its visibility.

Step 1: Include Placeholder/Loading Layout in Your Main Layout

Include the loading state layout in your main XML layout file (e.g., activity_main.xml):

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/contentTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Content Loaded"
        android:visibility="gone"/>

    <include
        android:id="@+id/loadingView"
        layout="@layout/loading_state"
        android:visibility="visible"/>

</RelativeLayout>

Initially, the loadingView is visible.

Step 2: Handle Visibility Programmatically

In your Activity or Fragment, handle the visibility changes:

import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    private View loadingView;
    private TextView contentTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        contentTextView = findViewById(R.id.contentTextView);
        loadingView = findViewById(R.id.loadingView);

        // Simulate loading data
        new Handler().postDelayed(() -> {
            loadingView.setVisibility(View.GONE);
            contentTextView.setVisibility(View.VISIBLE);
        }, 2000); // Simulate 2 seconds loading
    }
}

This method is straightforward but can lead to more initial layout inflation overhead compared to using ViewStub.

3. Using State List Drawables for More Complex Scenarios

For cases requiring more complex placeholder states (e.g., error states, empty states), State List Drawables can be employed. State List Drawables are XML files that define different images or backgrounds based on the current state of the view.

Step 1: Define State List Drawable

Create an XML file in the drawable directory, such as placeholder_state.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:state_empty="true">
        <!-- Empty State -->
        <layer-list>
            <item>
                <shape android:shape="rectangle">
                    <solid android:color="#E0E0E0"/> <!-- Grey background -->
                </shape>
            </item>
            <item android:gravity="center">
                <TextView
                    xmlns:android="http://schemas.android.com/apk/res/android"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="No data available"
                    android:textAppearance="?android:attr/textAppearanceMedium"/>
            </item>
        </layer-list>
    </item>

    <item android:state_error="true">
        <!-- Error State -->
        <layer-list>
            <item>
                <shape android:shape="rectangle">
                    <solid android:color="#F44336"/> <!-- Red background -->
                </shape>
            </item>
            <item android:gravity="center">
                <TextView
                    xmlns:android="http://schemas.android.com/apk/res/android"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Error loading data"
                    android:textColor="@android:color/white"
                    android:textAppearance="?android:attr/textAppearanceMedium"/>
            </item>
        </layer-list>
    </item>

    <item>
        <!-- Default State -->
        <color android:color="@android:color/transparent"/> <!-- Transparent by default -->
    </item>

</selector>
Step 2: Apply State List Drawable to a View

Apply this drawable to a view in your layout. For example:

<TextView
    android:id="@+id/stateTextView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/placeholder_state"
    android:text="Initial Content"
    android:gravity="center"/>
Step 3: Programmatically Set States

Programmatically update the view’s state (e.g., setting an error or empty state):

import android.graphics.drawable.StateListDrawable;
import android.os.Bundle;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    private TextView stateTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        stateTextView = findViewById(R.id.stateTextView);

        //Simulate Error State
        setErrorState();

        // Simulate Empty State
        // setEmptyState();
    }

    private void setErrorState() {
        stateTextView.setError(true);
        stateTextView.refreshDrawableState(); // Refresh the view's state to apply changes
    }


    private void setEmptyState() {
        stateTextView.setEmpty(true);  // This may require custom TextView class
        stateTextView.refreshDrawableState();
    }

}

Important Note: In standard Android TextView, state-related attributes like android:state_empty do not exist directly. A custom TextView or a more generic View might be necessary, or one may simply modify the content or visibility as per the requirement directly instead of attempting to use empty or other custom states. More elaborate view state approaches will involve significantly more detailed customization that might involve subclassing.

4. Shimmer Effect using Third-Party Libraries

A “shimmer” or “skeleton” effect can be added to provide an engaging visual loading experience. Implement the effect using third-party libraries like Facebook Shimmer.

Step 1: Add Shimmer Dependency

Add the Shimmer dependency to your build.gradle file:

dependencies {
    implementation 'com.facebook.shimmer:shimmer:0.5.0'
}
Step 2: Create Shimmer Layout

Wrap the views with a ShimmerFrameLayout to enable the effect. The loading elements can use placeholder shapes (e.g., gray rectangles) to indicate content will be loaded.

<com.facebook.shimmer.ShimmerFrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/shimmer_view_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <View
            android:layout_width="match_parent"
            android:layout_height="10dp"
            android:background="#E0E0E0"
            android:layout_margin="5dp"/>

        <View
            android:layout_width="match_parent"
            android:layout_height="10dp"
            android:background="#E0E0E0"
            android:layout_margin="5dp"/>

    </LinearLayout>

</com.facebook.shimmer.ShimmerFrameLayout>
Step 3: Start and Stop Shimmer Effect Programmatically

Control the shimmer effect programmatically to match your data loading status. Implement these functionalities in your Activity or Fragment:

import android.os.Bundle;
import android.os.Handler;
import androidx.appcompat.app.AppCompatActivity;
import com.facebook.shimmer.ShimmerFrameLayout;
import android.view.View;

public class MainActivity extends AppCompatActivity {

    private ShimmerFrameLayout shimmerFrameLayout;
    private View contentView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        shimmerFrameLayout = findViewById(R.id.shimmer_view_container);
        contentView = findViewById(R.id.content_view);  //Reference to your content view

        shimmerFrameLayout.startShimmer();  // Start the shimmer effect

        // Simulate data loading
        new Handler().postDelayed(() -> {
            shimmerFrameLayout.stopShimmer();
            shimmerFrameLayout.setVisibility(View.GONE);  // Hide shimmer layout
            contentView.setVisibility(View.VISIBLE);       // Show content view

        }, 2000); // Simulate 2 seconds loading
    }

    @Override
    public void onResume() {
        super.onResume();
        shimmerFrameLayout.startShimmer(); //Ensure Shimmer Starts if activity resumes
    }

    @Override
    public void onPause() {
        shimmerFrameLayout.stopShimmer();    //Stop Shimmer when activity pauses
        super.onPause();
    }
}

Make sure the `content_view` you have in the XML (activity_main.xml in our case) refers to actual main layout/content area so you make it visible upon completion of data load simulation.

Best Practices

  • Keep it Brief: Loading states should not last too long. Optimize your data loading to minimize delays.
  • Be Informative: Use text to indicate what is being loaded (e.g., “Loading images…”, “Fetching data…”).
  • Use Animations: Subtle animations can make loading states more engaging.
  • Error Handling: Include error states to inform users when something goes wrong.
  • Accessibility: Ensure your loading states are accessible to users with disabilities (e.g., provide appropriate ARIA attributes).
  • Consistency: Maintain a consistent design language across your app for all loading and placeholder states.

Conclusion

Designing placeholder and loading states in XML is crucial for enhancing the user experience in Android applications. Whether you choose ViewStub, visibility toggling, State List Drawables or Shimmer effects, the key is to provide clear and engaging feedback during data loading processes. Applying the best practices ensures your app feels responsive and user-friendly, regardless of data retrieval latency. By implementing these techniques effectively, you can significantly improve user satisfaction and overall app quality.