Creating Loading Indicators in Flutter

05 Dec 2025

9K

35K

Creating Loading Indicators in Flutter

Loading indicators are crucial UI elements that provide visual feedback to users when an application is performing an asynchronous operation, such as fetching data from an API, saving information, or processing a task. They prevent the user from thinking the app has frozen and improve the overall user experience. Flutter offers several built-in widgets for creating these indicators, as well as robust capabilities for custom implementations.

Why Loading Indicators Matter

  • User Feedback: Informs the user that an operation is in progress.
  • Improved UX: Reduces user frustration and perceived wait times.
  • Prevents Confusion: Makes it clear the app hasn't crashed or frozen.
  • Guided Interaction: Can indicate that input is temporarily paused.

Basic Loading Indicators

Flutter provides two primary built-in progress indicators: CircularProgressIndicator and LinearProgressIndicator.

1. CircularProgressIndicator

This widget displays a circular animation, typically used for indeterminate progress (when you don't know how long an operation will take) or determinate progress (when you can show a percentage completion).

Indeterminate CircularProgressIndicator:


import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Loading Indicator Demo')),
        body: const Center(
          child: CircularProgressIndicator(), // Basic indeterminate indicator
        ),
      ),
    );
  }
}
    

Determinate CircularProgressIndicator:

For determinate progress, you provide a value between 0.0 and 1.0.


CircularProgressIndicator(
  value: 0.7, // 70% complete
  backgroundColor: Colors.grey[200],
  valueColor: const AlwaysStoppedAnimation(Colors.blue),
)
    

2. LinearProgressIndicator

This widget displays a horizontal line animation, also usable for both indeterminate and determinate progress.

Indeterminate LinearProgressIndicator:


LinearProgressIndicator(
  backgroundColor: Colors.grey[200],
  valueColor: const AlwaysStoppedAnimation(Colors.green),
)
    

Determinate LinearProgressIndicator:


LinearProgressIndicator(
  value: 0.4, // 40% complete
  backgroundColor: Colors.grey[200],
  valueColor: const AlwaysStoppedAnimation(Colors.orange),
)
    

Conditional Display of Loading Indicators

In real-world applications, loading indicators are typically shown only when an asynchronous operation is in progress and hidden once it completes. This can be managed using state management techniques, often involving a boolean flag.

Using setState

A common approach for simple cases is to use setState within a StatefulWidget.


import 'package:flutter/material.dart';

class DataLoaderScreen extends StatefulWidget {
  const DataLoaderScreen({super.key});

  @override
  State createState() => _DataLoaderScreenState();
}

class _DataLoaderScreenState extends State {
  bool _isLoading = false;
  String _data = "No data loaded yet.";

  Future _fetchData() async {
    setState(() {
      _isLoading = true;
      _data = "Loading...";
    });

    // Simulate a network request
    await Future.delayed(const Duration(seconds: 3));

    setState(() {
      _data = "Data loaded successfully!";
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Conditional Loading')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (_isLoading)
              const CircularProgressIndicator()
            else
              Text(_data),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: _isLoading ? null : _fetchData,
              child: const Text('Load Data'),
            ),
          ],
        ),
      ),
    );
  }
}
    

Using FutureBuilder

For operations that return a Future, Flutter's FutureBuilder widget is an elegant and powerful solution. It rebuilds its UI based on the state of a Future (waiting, active, done).


import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('FutureBuilder Demo')),
        body: const FutureBuilderExample(),
      ),
    );
  }
}

class FutureBuilderExample extends StatefulWidget {
  const FutureBuilderExample({super.key});

  @override
  State createState() => _FutureBuilderExampleState();
}

class _FutureBuilderExampleState extends State {
  late Future _dataFuture;

  @override
  void initState() {
    super.initState();
    _dataFuture = _fetchData();
  }

  Future _fetchData() async {
    // Simulate a network request
    await Future.delayed(const Duration(seconds: 3));
    return "Data from Future loaded!";
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: FutureBuilder(
        future: _dataFuture,
        builder: (BuildContext context, AsyncSnapshot snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                CircularProgressIndicator(),
                SizedBox(height: 16),
                Text('Loading data...'),
              ],
            );
          } else if (snapshot.hasError) {
            return Text('Error: ${snapshot.error}');
          } else if (snapshot.hasData) {
            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(snapshot.data!),
                const SizedBox(height: 16),
                ElevatedButton(
                  onPressed: () {
                    setState(() {
                      _dataFuture = _fetchData(); // Reload data
                    });
                  },
                  child: const Text('Reload Data'),
                ),
              ],
            );
          } else {
            return const Text('No data');
          }
        },
      ),
    );
  }
}
    

Custom Loading Indicators

While CircularProgressIndicator and LinearProgressIndicator cover most use cases, you might want a more unique or branded loading experience. Flutter's powerful rendering capabilities allow you to create custom loaders using widgets like AnimatedBuilder, Transform, CustomPaint, or by combining multiple widgets.

For instance, you could create a pulsating effect:


import 'package:flutter/material.dart';

class PulsingLoader extends StatefulWidget {
  const PulsingLoader({super.key});

  @override
  State createState() => _PulsingLoaderState();
}

class _PulsingLoaderState extends State with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    )..repeat(reverse: true);
    _animation = Tween(begin: 0.8, end: 1.2).animate(_controller);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ScaleTransition(
      scale: _animation,
      child: Container(
        width: 50,
        height: 50,
        decoration: BoxDecoration(
          color: Colors.blue.withOpacity(0.7),
          shape: BoxShape.circle,
        ),
      ),
    );
  }
}
    

This custom loader can then be used in place of the standard indicators.

Conclusion

Loading indicators are essential for creating a smooth and user-friendly experience in Flutter applications. Whether you use the built-in CircularProgressIndicator and LinearProgressIndicator, leverage FutureBuilder for asynchronous operations, or craft entirely custom animations, Flutter provides all the tools you need to effectively communicate application state to your users. Choosing the right indicator and implementing it conditionally will significantly enhance your app's perceived performance and usability.

Related Articles

Aug 02, 2026

Building a Flutter Notification Center with Grouping & Actions

Learn to create a sophisticated in-app notification center in Flutter, featuring grouping, swipe-to-dismiss, custom icons, and actionable buttons for enhanced u

Aug 01, 2026

Flutter Layout Tips: Using FractionallySizedBox, AspectRatio, ConstrainedBox for Adaptive UI

Master Flutter's FractionallySizedBox, AspectRatio, and ConstrainedBox to build truly adaptive UIs. Learn how to control widget sizing, maintain proportions, an

Aug 01, 2026

Flutter Product Quick View: Animation, Related Items, Badges

Learn to create a dynamic product quick view widget in Flutter, incorporating smooth animations, relevant related items, and eye-catching promo badges for an en