Popular Searches
Popular Course Categories
Popular Courses

API Loading and Error Handling

API Loading and Error Handling

Flutter APIs & Networking

API Loading and Error Handling in Flutter

When a Flutter application communicates with an API, the request may take some time to complete, fail because of network problems, return an unexpected status code, or provide invalid data. A good Flutter application should clearly handle loading, success, empty, and error states so that users always understand what is happening.

Flutter commonly uses the http package for HTTP requests and FutureBuilder for displaying asynchronous loading, success, and error states.


1. What is API Loading?

API loading is the state displayed while Flutter is waiting for a response from a server. Because network requests are asynchronous, the application should not appear frozen while waiting for the response.

Typical loading indicators include:

  • CircularProgressIndicator
  • LinearProgressIndicator
  • Loading text
  • Skeleton screens
  • Placeholder cards

2. Why is Loading Handling Important?

  • It informs the user that the request is in progress.
  • It prevents users from thinking that the application has stopped responding.
  • It improves the overall user experience.
  • It provides feedback during slow network connections.
  • It makes asynchronous API operations easier to understand.

3. Basic API Request

First, add the http package to your Flutter project:

flutter pub add http

Import the package:

import 'package:http/http.dart' as http;

The http package provides a simple way to make HTTP requests from Flutter applications.

4. Understanding Future in API Calls

An HTTP request does not normally return its final result immediately. Dart represents asynchronous operations using Future.

Future<http.Response> fetchData() {
  return http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
  );
}

A Future represents a value or error that becomes available later.

5. Understanding async and await

The async and await keywords make asynchronous API code easier to read.

Future<String> fetchData() async {
  final response = await http.get(
    Uri.parse('https://example.com/api/data'),
  );

  return response.body;
}

The await keyword waits for the Future to complete without blocking the application UI thread.

6. Basic Loading with FutureBuilder

FutureBuilder is useful for building widgets based on the current state of a Future. A common pattern is to show a loading indicator while waiting, display an error when snapshot.hasError is true, and display data when snapshot.hasData is true.

FutureBuilder<String>(
  future: fetchData(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const CircularProgressIndicator();
    }

    if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    }

    if (snapshot.hasData) {
      return Text(snapshot.data!);
    }

    return const Text('No data available');
  },
)

7. FutureBuilder Connection States

FutureBuilder provides a connectionState that can be used to determine the current stage of an asynchronous operation.

Connection State Meaning Typical UI
none No asynchronous operation is currently connected. Initial or empty UI
waiting The Future is still running. Loading indicator
active The asynchronous source is active. Progress UI where applicable
done The Future has completed. Success or error UI

8. Checking ConnectionState

FutureBuilder<List<String>>(
  future: fetchItems(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }

    if (snapshot.hasError) {
      return Center(
        child: Text('Error: ${snapshot.error}'),
      );
    }

    if (snapshot.hasData) {
      final items = snapshot.data!;

      return ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(items[index]),
          );
        },
      );
    }

    return const Center(
      child: Text('No data available'),
    );
  },
)

9. Complete API Loading Example

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

Future<Map<String, dynamic>> fetchUser() async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/users/1'),
  );

  if (response.statusCode == 200) {
    return jsonDecode(response.body) as Map<String, dynamic>;
  }

  throw Exception('Failed to load user');
}

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

  @override
  State<UserPage> createState() => _UserPageState();
}

class _UserPageState extends State<UserPage> {
  late Future<Map<String, dynamic>> userFuture;

  @override
  void initState() {
    super.initState();
    userFuture = fetchUser();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('User Details'),
      ),
      body: FutureBuilder<Map<String, dynamic>>(
        future: userFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }

          if (snapshot.hasError) {
            return Center(
              child: Text('Error: ${snapshot.error}'),
            );
          }

          if (snapshot.hasData) {
            final user = snapshot.data!;

            return Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    user['name'] as String,
                    style: const TextStyle(
                      fontSize: 24,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  Text(user['email'] as String),
                ],
              ),
            );
          }

          return const Center(
            child: Text('No user data found'),
          );
        },
      ),
    );
  }
}

10. Why Should API Calls Not Be Made Inside build()?

The build() method can execute many times during the lifetime of a widget. Starting an API request directly inside build() can therefore cause repeated requests and unnecessary network traffic.

A common approach is to initialize the Future in initState() and then provide that Future to FutureBuilder.

late Future<Map<String, dynamic>> userFuture;

@override
void initState() {
  super.initState();
  userFuture = fetchUser();
}

11. What is API Error Handling?

API error handling means detecting and responding to problems that occur while communicating with a server.

Errors can happen because of:

  • No internet connection
  • Slow or unstable network
  • Server errors
  • Invalid API URLs
  • Authentication failures
  • Invalid request parameters
  • Unexpected response data
  • JSON parsing problems
  • Request timeouts

12. HTTP Status Codes

An API response contains an HTTP status code. Your application should inspect the status code before treating the response as successful.

Status Code Meaning Typical Flutter Response
200 OK Process response
201 Created Data successfully created
400 Bad Request Show request validation message
401 Unauthorized Ask user to authenticate
403 Forbidden Show permission message
404 Not Found Show resource-not-found message
500 Internal Server Error Show server error message
503 Service Unavailable Ask user to retry later

13. Checking the Status Code

Future<String> fetchData() async {
  final response = await http.get(
    Uri.parse('https://example.com/api/data'),
  );

  if (response.statusCode == 200) {
    return response.body;
  }

  throw Exception(
    'Request failed with status code: ${response.statusCode}',
  );
}

14. Using try-catch for Network Errors

The try-catch statement can be used to catch exceptions generated during asynchronous operations.

Future<String> fetchData() async {
  try {
    final response = await http.get(
      Uri.parse('https://example.com/api/data'),
    );

    if (response.statusCode == 200) {
      return response.body;
    }

    throw Exception(
      'Server returned ${response.statusCode}',
    );
  } catch (e) {
    throw Exception('Network request failed: $e');
  }
}

15. Displaying a User-Friendly Error

Technical error messages are useful for developers, but users generally need simple and understandable messages.

if (snapshot.hasError) {
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        const Icon(
          Icons.error_outline,
          size: 50,
        ),
        const SizedBox(height: 12),
        const Text(
          'Unable to load data.',
          style: TextStyle(fontSize: 18),
        ),
        const SizedBox(height: 8),
        ElevatedButton(
          onPressed: retry,
          child: const Text('Try Again'),
        ),
      ],
    ),
  );
}

16. Retry API Requests

A retry button allows the user to execute the request again after an error.

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

  @override
  State<UserPage> createState() => _UserPageState();
}

class _UserPageState extends State<UserPage> {
  late Future<Map<String, dynamic>> userFuture;

  @override
  void initState() {
    super.initState();
    loadUser();
  }

  void loadUser() {
    userFuture = fetchUser();
  }

  void retry() {
    setState(() {
      loadUser();
    });
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Map<String, dynamic>>(
      future: userFuture,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Center(
            child: CircularProgressIndicator(),
          );
        }

        if (snapshot.hasError) {
          return Center(
            child: ElevatedButton(
              onPressed: retry,
              child: const Text('Try Again'),
            ),
          );
        }

        if (snapshot.hasData) {
          return Text(snapshot.data!['name'] as String);
        }

        return const Text('No data');
      },
    );
  }
}

17. Loading, Success, Empty, and Error States

A robust API screen commonly handles four major states:

State Condition UI Example
Loading Request is running Progress indicator
Success Valid data received Display API data
Empty Request succeeds but no records exist No data message
Error Request or parsing fails Error message + retry

18. Handling an Empty List

if (snapshot.hasData) {
  final users = snapshot.data!;

  if (users.isEmpty) {
    return const Center(
      child: Text('No users found'),
    );
  }

  return ListView.builder(
    itemCount: users.length,
    itemBuilder: (context, index) {
      return ListTile(
        title: Text(users[index].name),
      );
    },
  );
}

19. Complete Loading and Error UI

FutureBuilder<List<User>>(
  future: usersFuture,
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }

    if (snapshot.hasError) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Something went wrong.'),
            const SizedBox(height: 10),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  usersFuture = fetchUsers();
                });
              },
              child: const Text('Retry'),
            ),
          ],
        ),
      );
    }

    if (!snapshot.hasData || snapshot.data!.isEmpty) {
      return const Center(
        child: Text('No users available.'),
      );
    }

    final users = snapshot.data!;

    return ListView.builder(
      itemCount: users.length,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text(users[index].name),
        );
      },
    );
  },
)

20. Handling JSON Parsing Errors

An API may return data that does not match the expected JSON structure. Model parsing code should detect invalid data and throw an appropriate exception.

class User {
  final int id;
  final String name;

  User({
    required this.id,
    required this.name,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    if (json['id'] is! int || json['name'] is! String) {
      throw const FormatException(
        'Invalid user data',
      );
    }

    return User(
      id: json['id'] as int,
      name: json['name'] as String,
    );
  }
}

21. Handling Different Error Types

It is useful to distinguish between different failures so that the application can provide more meaningful messages.

Future<List<User>> fetchUsers() async {
  try {
    final response = await http.get(
      Uri.parse('https://example.com/api/users'),
    );

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);

      if (data is! List) {
        throw const FormatException(
          'Invalid response format',
        );
      }

      return data
          .map((item) => User.fromJson(
                item as Map<String, dynamic>,
              ))
          .toList();
    }

    if (response.statusCode == 401) {
      throw Exception('Authentication required');
    }

    if (response.statusCode == 404) {
      throw Exception('Users endpoint not found');
    }

    if (response.statusCode >= 500) {
      throw Exception('Server error');
    }

    throw Exception(
      'Request failed: ${response.statusCode}',
    );
  } on FormatException {
    throw Exception('Invalid data received from server');
  } catch (e) {
    throw Exception('Unable to connect to server');
  }
}

22. Showing Different Messages for Different Errors

String getErrorMessage(Object? error) {
  final message = error.toString();

  if (message.contains('Authentication')) {
    return 'Please log in again.';
  }

  if (message.contains('Server')) {
    return 'The server is currently unavailable.';
  }

  if (message.contains('Invalid')) {
    return 'The server returned invalid data.';
  }

  return 'Please check your internet connection and try again.';
}

23. Using LinearProgressIndicator

A linear progress indicator can be useful when the loading state should appear at the top of a page.

Column(
  children: [
    const LinearProgressIndicator(),
    Expanded(
      child: ListView(
        children: const [
          ListTile(
            title: Text('Loading...'),
          ),
        ],
      ),
    ),
  ],
)

24. Using a Loading Overlay

For operations such as submitting forms, a loading overlay can prevent users from repeatedly pressing a button.

Stack(
  children: [
    Form(
      child: Column(
        children: [
          TextFormField(),
          ElevatedButton(
            onPressed: isLoading ? null : submitForm,
            child: const Text('Submit'),
          ),
        ],
      ),
    ),
    if (isLoading)
      Container(
        color: Colors.black26,
        child: const Center(
          child: CircularProgressIndicator(),
        ),
      ),
  ],
)

25. Preventing Multiple API Requests

Users may accidentally tap a button multiple times while a request is running. Disable the action while loading.

ElevatedButton(
  onPressed: isLoading ? null : submitForm,
  child: isLoading
      ? const SizedBox(
          width: 20,
          height: 20,
          child: CircularProgressIndicator(),
        )
      : const Text('Submit'),
)

26. Refreshing API Data

API data can be refreshed by assigning a new Future and rebuilding the widget.

Future<void> refreshUsers() async {
  setState(() {
    usersFuture = fetchUsers();
  });
}

27. Pull-to-Refresh

RefreshIndicator provides a familiar pull-to-refresh interaction for scrollable content.

RefreshIndicator(
  onRefresh: () async {
    setState(() {
      usersFuture = fetchUsers();
    });

    await usersFuture;
  },
  child: ListView.builder(
    itemCount: users.length,
    itemBuilder: (context, index) {
      return ListTile(
        title: Text(users[index].name),
      );
    },
  ),
)

28. API Timeout Handling

A timeout can prevent an application from waiting indefinitely for a server response.

Future<http.Response> fetchData() async {
  return await http
      .get(
        Uri.parse('https://example.com/api/data'),
      )
      .timeout(
        const Duration(seconds: 10),
      );
}

The timeout can be caught and converted into a user-friendly message.

try {
  final response = await fetchData();
  return response;
} on TimeoutException {
  throw Exception(
    'The request took too long. Please try again.',
  );
}

29. Complete API Service Example

import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;

class ApiService {
  static const String baseUrl =
      'https://jsonplaceholder.typicode.com';

  Future<List<Map<String, dynamic>>> fetchUsers() async {
    try {
      final response = await http
          .get(
            Uri.parse('$baseUrl/users'),
          )
          .timeout(
            const Duration(seconds: 10),
          );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);

        if (data is! List) {
          throw const FormatException(
            'Invalid response format',
          );
        }

        return List<Map<String, dynamic>>.from(
          data.map(
            (item) => Map<String, dynamic>.from(item as Map),
          ),
        );
      }

      throw Exception(
        'Server returned ${response.statusCode}',
      );
    } on TimeoutException {
      throw Exception('Request timed out');
    } on FormatException {
      throw Exception('Invalid data received');
    } catch (e) {
      throw Exception('Unable to fetch users');
    }
  }
}

30. API Service with Flutter UI

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

  @override
  State<UsersPage> createState() => _UsersPageState();
}

class _UsersPageState extends State<UsersPage> {
  final ApiService apiService = ApiService();

  late Future<List<Map<String, dynamic>>> usersFuture;

  @override
  void initState() {
    super.initState();
    usersFuture = apiService.fetchUsers();
  }

  void retry() {
    setState(() {
      usersFuture = apiService.fetchUsers();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Users'),
      ),
      body: FutureBuilder<List<Map<String, dynamic>>>(
        future: usersFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState ==
              ConnectionState.waiting) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }

          if (snapshot.hasError) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Text(
                    'Unable to load users.',
                  ),
                  const SizedBox(height: 10),
                  ElevatedButton(
                    onPressed: retry,
                    child: const Text('Retry'),
                  ),
                ],
              ),
            );
          }

          final users = snapshot.data ?? [];

          if (users.isEmpty) {
            return const Center(
              child: Text('No users found.'),
            );
          }

          return ListView.builder(
            itemCount: users.length,
            itemBuilder: (context, index) {
              final user = users[index];

              return ListTile(
                title: Text(user['name'] as String),
                subtitle: Text(user['email'] as String),
              );
            },
          );
        },
      ),
    );
  }
}

31. Error Handling Flow

User opens screen
       |
       v
Start API request
       |
       v
Show Loading UI
       |
       v
Receive response
       |
       +--------------------+
       |                    |
       v                    v
   Success                Error
       |                    |
       v                    v
 Parse JSON           Identify error
       |                    |
       v                    v
 Check data           Show message
       |                    |
       v                    v
 Display UI           Retry option

32. Common API Errors and Solutions

Problem Possible Cause Solution
Loading forever Future never completes or result is not handled correctly Handle success and error states and ensure the Future completes
404 error Incorrect endpoint or resource Check API URL and resource ID
401 error Authentication missing or expired Authenticate or refresh credentials
500 error Server-side problem Display a server error and provide retry
JSON parsing error Unexpected response structure Validate the response before parsing
Timeout Slow server or network Set a timeout and allow retry
Repeated requests API called from build() Store the Future in state
App appears frozen No loading feedback Display a progress indicator

33. Common Mistakes to Avoid

  • Do not start API requests repeatedly inside build().
  • Do not assume every HTTP response is successful.
  • Do not display only a spinner without an error state.
  • Do not display confusing technical error messages directly to users.
  • Do not ignore JSON parsing failures.
  • Do not allow users to repeatedly submit the same request while it is loading.
  • Do not leave an API screen without an empty-data state.
  • Do not ignore timeout and network failures.
  • Do not assume that a successful HTTP request always means useful application data was returned.

34. Best Practices for API Loading and Error Handling

  1. Keep API code separate from UI code when possible.
  2. Use Future and async/await for asynchronous operations.
  3. Check HTTP status codes.
  4. Use try-catch for exceptions.
  5. Display a clear loading state.
  6. Display a meaningful error state.
  7. Provide a retry action when appropriate.
  8. Handle empty API responses.
  9. Validate JSON before converting it to models.
  10. Avoid making API calls directly inside build().
  11. Use timeouts for requests that should not wait indefinitely.
  12. Disable duplicate actions while a request is in progress.
  13. Keep technical error details in logs while showing simple messages to users.

35. Loading and Error Handling with a Custom State

For larger applications, a custom state can make the UI easier to maintain.

enum ApiState {
  loading,
  success,
  empty,
  error,
}

ApiState state = ApiState.loading;

The UI can then be selected according to the current state:

switch (state) {
  case ApiState.loading:
    return const CircularProgressIndicator();

  case ApiState.success:
    return const Text(
      'Data loaded successfully',
    );

  case ApiState.empty:
    return const Text(
      'No data available',
    );

  case ApiState.error:
    return ElevatedButton(
      onPressed: retry,
      child: const Text('Retry'),
    );
}

36. API Loading State Example

bool isLoading = false;

Future<void> loadData() async {
  setState(() {
    isLoading = true;
  });

  try {
    final data = await fetchData();

    // Process data here.
  } catch (e) {
    // Handle error here.
  } finally {
    if (mounted) {
      setState(() {
        isLoading = false;
      });
    }
  }
}

37. Why finally is Useful

The finally block is useful when an operation must be performed regardless of whether the request succeeds or fails. For example, a loading flag should normally be reset after both successful and failed requests.

38. Error Handling at the Application Level

Flutter also provides mechanisms for handling framework-level errors. FlutterError.onError can handle errors routed through Flutter's framework callbacks, while asynchronous errors outside those callbacks can be handled using PlatformDispatcher.instance.onError.

import 'dart:ui';
import 'package:flutter/material.dart';

void main() {
  FlutterError.onError = (details) {
    FlutterError.presentError(details);
  };

  PlatformDispatcher.instance.onError = (error, stack) {
    debugPrint('Unhandled error: $error');
    return true;
  };

  runApp(const MyApp());
}

39. API Loading and Error Handling Architecture

Flutter UI
   |
   v
View / Screen
   |
   v
ViewModel / Controller
   |
   v
API Service
   |
   v
HTTP Client
   |
   v
Backend API
   |
   v
Response
   |
   +------ Success ------> Model ------> UI
   |
   +------ Error --------> Exception ---> UI Error State

40. Practical Example: Product List

Suppose an application loads products from an API. The screen should handle all major states:

FutureBuilder<List<Product>>(
  future: productFuture,
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }

    if (snapshot.hasError) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Unable to load products.'),
            ElevatedButton(
              onPressed: retry,
              child: const Text('Retry'),
            ),
          ],
        ),
      );
    }

    if (!snapshot.hasData || snapshot.data!.isEmpty) {
      return const Center(
        child: Text('No products available.'),
      );
    }

    final products = snapshot.data!;

    return ListView.builder(
      itemCount: products.length,
      itemBuilder: (context, index) {
        final product = products[index];

        return Card(
          child: ListTile(
            title: Text(product.name),
            subtitle: Text(product.price.toString()),
          ),
        );
      },
    );
  },
)

41. Practical API Loading and Error Handling Example

A practical application should combine loading, success, empty, error, retry, and refresh behavior into a single predictable flow.

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

  @override
  State<ProductsPage> createState() => _ProductsPageState();
}

class _ProductsPageState extends State<ProductsPage> {
  late Future<List<Product>> productsFuture;

  @override
  void initState() {
    super.initState();
    productsFuture = fetchProducts();
  }

  void retry() {
    setState(() {
      productsFuture = fetchProducts();
    });
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<Product>>(
      future: productsFuture,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Center(
            child: CircularProgressIndicator(),
          );
        }

        if (snapshot.hasError) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Text('Unable to load products.'),
                const SizedBox(height: 12),
                ElevatedButton(
                  onPressed: retry,
                  child: const Text('Retry'),
                ),
              ],
            ),
          );
        }

        final products = snapshot.data ?? [];

        if (products.isEmpty) {
          return const Center(
            child: Text('No products available.'),
          );
        }

        return ListView.builder(
          itemCount: products.length,
          itemBuilder: (context, index) {
            final product = products[index];

            return Card(
              child: ListTile(
                title: Text(product.name),
                subtitle: Text(
                  product.price.toString(),
                ),
              ),
            );
          },
        );
      },
    );
  }
}

42. API Error Handling Checklist

  • Show loading feedback before or while the request is running.
  • Check the HTTP status code.
  • Parse JSON safely.
  • Catch network exceptions.
  • Handle timeouts.
  • Handle empty responses.
  • Show a user-friendly error message.
  • Provide retry functionality.
  • Prevent duplicate requests.
  • Do not call APIs repeatedly from build().
  • Keep API and UI responsibilities separated.

43. Recommended Development Process

  1. Create an API service method.
  2. Define the expected response model.
  3. Make the HTTP request using http.
  4. Check the response status code.
  5. Decode and validate the JSON response.
  6. Convert JSON into Dart models.
  7. Expose the API result as a Future.
  8. Store the Future outside the build() method.
  9. Display a loading state.
  10. Display the successful response.
  11. Handle empty data.
  12. Handle exceptions and errors.
  13. Add retry functionality where appropriate.
  14. Add timeout and refresh behavior where required.

44. Interview Questions

  1. What is API loading in Flutter?
  2. Why do Flutter applications need loading indicators?
  3. What is a Future in Dart?
  4. What is FutureBuilder?
  5. How do you check whether a Future is still loading?
  6. What is the difference between snapshot.hasData and snapshot.hasError?
  7. Why should API calls generally not be placed directly inside build()?
  8. How do you handle HTTP status codes?
  9. How does try-catch help with API requests?
  10. How can you implement a retry button?
  11. How can you handle an empty API response?
  12. How do you handle JSON parsing errors?
  13. How can you implement request timeout handling?
  14. How can you prevent duplicate API requests?
  15. What is the purpose of finally in an API request?
  16. What is the difference between loading, success, empty, and error states?
  17. Why should technical error messages generally not be shown directly to end users?

45. Practice Exercise

Create a Flutter application that loads a list of users from an API.

  1. Create a Flutter project.
  2. Add the http package.
  3. Create an API service class.
  4. Make a GET request.
  5. Check the HTTP status code.
  6. Parse the JSON response.
  7. Create a User model.
  8. Display a loading indicator while the request is running.
  9. Display users when the request succeeds.
  10. Display an empty message when no users are returned.
  11. Display an error message when the request fails.
  12. Add a Retry button.
  13. Add pull-to-refresh functionality.
  14. Add timeout handling.
  15. Disable duplicate requests where necessary.

46. Quick Revision

Concept Purpose
Future Represents an asynchronous result
async Marks a function as asynchronous
await Waits for a Future to complete
FutureBuilder Builds UI according to a Future's state
ConnectionState.waiting Indicates that the Future is still running
snapshot.hasData Checks whether non-null data is available
snapshot.hasError Checks whether the Future completed with an error
try-catch Catches exceptions
statusCode Indicates the HTTP response status
RefreshIndicator Provides pull-to-refresh behavior
Timeout Stops waiting after a specified duration
Retry Allows the user to execute a failed request again
Empty State Communicates that the request succeeded but no records are available

47. Learning Outcome

After completing this topic, you should be able to:

  • Understand asynchronous API loading in Flutter.
  • Use Future, async, and await.
  • Use FutureBuilder to manage asynchronous UI states.
  • Handle loading, success, empty, and error states.
  • Check HTTP status codes.
  • Handle network and parsing exceptions.
  • Implement retry functionality.
  • Implement pull-to-refresh.
  • Handle request timeouts.
  • Prevent duplicate API requests.
  • Separate API services from UI code.
  • Build more reliable API-driven Flutter applications.

48. Summary

API loading and error handling are essential parts of Flutter application development. A production-quality application should never assume that every network request succeeds. The UI should clearly communicate when data is loading, when data has been successfully received, when no data is available, and when an error has occurred.

Using http, Future, async/await, try-catch, status-code validation, and FutureBuilder provides a strong foundation for handling API-based Flutter applications.

Core flow:

API Request
    ↓
Loading State
    ↓
Receive Response
    ↓
Check Status Code
    ↓
Parse JSON
    ↓
Validate Data
    ↓
+-------------------------------+
|                               |
v                               v
Success                       Error
|                               |
v                               v
Display Data              Show Error
|                               |
v                               v
Empty Check                  Retry
    |
    v
Empty / Data UI

49. Useful Resources

50. JustAcademy Flutter Training

Learn Flutter development with structured practical training, including Flutter development, Dart programming, API integration, Firebase, UI development, and application development.

Explore JustAcademy Flutter Training Course

Register for Flutter Course Demo

Previous Next
whatsapp