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


API loading and error handling are essential parts of Flutter application development. When a Flutter application communicates with a backend API, the request may take time, fail because of network problems, return an unexpected HTTP status code, or provide invalid data. A good application should clearly manage loading, success, empty, and error states instead of leaving the user with a blank screen.


Flutter's http package can be used to make HTTP requests, while widgets such as FutureBuilder can help build UI according to asynchronous states. Flutter's official networking recipes demonstrate checking response status codes, converting responses into Dart objects, and displaying loading or error states. Flutter Fetch Data Documentation




1. What is API Loading?


API loading is the state in which a Flutter application is waiting for a response from a server.


For example, when a user opens a product screen, the application may need to request product data from an API. While the server is processing the request, Flutter should display a loading indicator.


Loading
   ↓
API Request
   ↓
Server Processing
   ↓
Response
   ↓
Success / Error

Examples of Loading Indicators



  • CircularProgressIndicator

  • LinearProgressIndicator

  • Skeleton loaders

  • Shimmer placeholders

  • Progress indicators inside buttons




2. Why API Loading and Error Handling Are Important



  • Internet requests are asynchronous.

  • Network connections can be slow or unavailable.

  • The server may return an error.

  • The API may return invalid or unexpected data.

  • Authentication may expire.

  • The requested resource may not exist.

  • The server may temporarily be unavailable.


Without proper handling, users may see a blank screen, an infinite loading indicator, or an application crash.




3. Common API States


A robust API-driven screen commonly manages these states:









State Meaning Typical UI
Initial No API request has started yet Initial screen or action button
Loading Request is currently running Progress indicator
Success Valid data was received Display API data
Empty Request succeeded but no useful records exist No data message
Error Request or processing failed Error message and retry button



4. Adding the HTTP Package


The http package provides a simple way to perform HTTP networking in Flutter.


flutter pub add http

Import it into your Dart file:


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

Flutter's official documentation uses the http package for examples involving GET, POST, PUT, and DELETE requests. Flutter Networking Cookbook


Android Internet Permission


For Android applications, ensure the application has internet permission in the manifest:


 



5. Making a Basic GET Request


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

 


Future fetchUser() {
  return http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/users/1'),
  );
}


The http.get() method returns a Future containing an http.Response. A Future represents a result that becomes available asynchronously. Flutter Fetch Data Documentation




6. Checking the HTTP Status Code


Never assume that every HTTP request was successful. Check the response status code before processing the response.


Future fetchData() async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/users/1'),
  );

 


  if (response.statusCode == 200) {
    return response.body;
  } else {
    throw Exception('Failed to load data');
  }
}


Flutter's networking examples similarly check the expected status code and throw an exception when the server does not return the expected response. :contentReference[oaicite:0]{index=0}




7. Important HTTP Status Codes














Status Code Meaning Possible Flutter Action
200 OK Process successful response
201 Created Process newly created resource
204 No Content Process successful response without body
400 Bad Request Show validation/request error
401 Unauthorized Ask user to authenticate again
403 Forbidden Show permission/access message
404 Not Found Show resource-not-found message
408 Request Timeout Allow retry
500 Internal Server Error Show server error and retry option
503 Service Unavailable Ask user to try again later



8. Handling JSON Responses


API responses are frequently returned as JSON. Use dart:convert to decode JSON.


import 'dart:convert';

 


final data = jsonDecode(response.body);


For example, if the API returns:


{
  "id": 1,
  "name": "Rahul",
  "email": "[email protected]"
}

You can convert it into a Dart model.


class User {
  final int id;
  final String name;
  final String email;

 


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


  factory User.fromJson(Map json) {
    return User(
      id: json['id'],
      name: json['name'],
      email: json['email'],
    );
  }
}




9. Creating a Reusable API Function


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

 


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


  if (response.statusCode == 200) {
    final json = jsonDecode(response.body);
    return User.fromJson(json);
  }


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


This approach keeps API communication separate from the UI and makes the code easier to test and maintain.




10. Using FutureBuilder for API Loading


FutureBuilder is useful when the UI depends on an asynchronous Future. It can display different widgets according to whether the Future has completed successfully or with an error.


FutureBuilder(
  future: fetchUser(),
  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!.name);
    }


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


Flutter's official networking documentation demonstrates the same general pattern: show a progress indicator while waiting, display data when available, and handle errors through snapshot.hasError. :contentReference[oaicite:1]{index=1}




11. Understanding ConnectionState


FutureBuilder provides a ConnectionState that describes the current asynchronous operation.








ConnectionState Meaning
none No asynchronous operation is currently connected
waiting Waiting for the Future to complete
active Active asynchronous interaction, more commonly relevant to streams
done The asynchronous operation has completed

Typical Loading Check


if (snapshot.connectionState == ConnectionState.waiting) {
  return const Center(
    child: CircularProgressIndicator(),
  );
}



12. Displaying a Loading Indicator


Center(
  child: CircularProgressIndicator(),
)

For a full-screen API request:


Scaffold(
  body: Center(
    child: CircularProgressIndicator(),
  ),
)

Button Loading State


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



13. Handling Network Errors


Network failures can occur because of no internet connection, DNS problems, unavailable servers, connection failures, or other networking issues.


try {
  final response = await http.get(
    Uri.parse('https://example.com/users'),
  );

 


  if (response.statusCode == 200) {
    print(response.body);
  } else {
    throw Exception('Server returned an error');
  }
} catch (e) {
  print('Network error: $e');
}


Instead of exposing technical exceptions directly to users, convert them into clear messages such as:



  • "Please check your internet connection."

  • "Unable to connect to the server."

  • "Something went wrong. Please try again."




14. Handling HTTP Errors


Future fetchUser() async {
  final response = await http.get(
    Uri.parse('https://example.com/users/1'),
  );

 


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


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


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


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


  throw Exception('Unexpected error occurred');
}




15. Handling JSON Parsing Errors


An API can return invalid JSON or JSON with an unexpected structure. Parsing should therefore be protected with error handling.


try {
  final data = jsonDecode(response.body);
  return User.fromJson(data);
} on FormatException {
  throw Exception('Invalid server response');
} catch (e) {
  throw Exception('Unable to process the response');
}



16. Handling Unexpected JSON Structure


Suppose your application expects:


{
  "id": 1,
  "name": "Rahul"
}

But the server returns:


{
  "user_id": 1,
  "full_name": "Rahul"
}

The model may fail to parse the response. Validate important fields when necessary.


factory User.fromJson(Map json) {
  if (json['id'] == null || json['name'] == null) {
    throw const FormatException('Invalid user data');
  }

 


  return User(
    id: json['id'],
    name: json['name'],
    email: json['email'] ?? '',
  );
}




17. Creating a Custom API Exception


Custom exceptions make error handling more organized.


class ApiException implements Exception {
  final String message;
  final int? statusCode;

 


  ApiException(this.message, {this.statusCode});


  @override
  String toString() {
    return message;
  }
}


Use it in the API service:


if (response.statusCode != 200) {
  throw ApiException(
    'Failed to load user',
    statusCode: response.statusCode,
  );
}



18. Handling Timeout Errors


A request should not wait forever. A timeout can be applied to a Future.


Future fetchData() async {
  try {
    final response = await http
        .get(Uri.parse('https://example.com/data'))
        .timeout(const Duration(seconds: 10));

 


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


    throw Exception('Server error');
  } catch (e) {
    throw Exception('Request failed or timed out');
  }
}


A timeout allows the application to recover instead of leaving the user on an indefinite loading screen.




19. Retry After an Error


A retry button is useful when an API request fails temporarily.


ElevatedButton(
  onPressed: () {
    setState(() {
      futureUser = fetchUser();
    });
  },
  child: const Text('Retry'),
)

Complete Retry UI


FutureBuilder(
  future: futureUser,
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }

 


    if (snapshot.hasError) {
      return Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text('Unable to load data'),
            const SizedBox(height: 10),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  futureUser = fetchUser();
                });
              },
              child: const Text('Retry'),
            ),
          ],
        ),
      );
    }


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


    return Text(snapshot.data!.name);
  },
)




20. Empty State vs Error State


An empty result is not necessarily an error.









Situation Example Message
Successful response with records Display records
Successful response with no records "No products found"
Network failure "Check your internet connection"
Server failure "Server is temporarily unavailable"
Authentication failure "Please log in again"



21. Loading, Success, Empty and Error UI


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

 


    if (snapshot.hasError) {
      return const Center(
        child: Text('Failed to load users'),
      );
    }


    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),
          subtitle: Text(users[index].email),
        );
      },
    );
  },
)




22. API Loading with setState


For smaller screens and simple applications, API state can also be managed using setState().


bool isLoading = false;
String? errorMessage;
List users = [];

 


Future loadUsers() async {
  setState(() {
    isLoading = true;
    errorMessage = null;
  });


  try {
    final result = await fetchUsers();


    if (!mounted) return;


    setState(() {
      users = result;
      isLoading = false;
    });
  } catch (e) {
    if (!mounted) return;


    setState(() {
      isLoading = false;
      errorMessage = 'Failed to load users';
    });
  }
}




23. Why mounted Matters


An asynchronous request may finish after a widget has been removed from the widget tree. Before calling setState() after an asynchronous operation, check whether the State object is still mounted.


final users = await fetchUsers();

 


if (!mounted) return;


setState(() {
  this.users = users;
});


This helps prevent updating a State object that is no longer active.




24. Avoid API Calls Inside build()


Do not normally create a new API request directly inside the build() method.


Incorrect:


@override
Widget build(BuildContext context) {
  return FutureBuilder(
    future: fetchUser(),
    builder: (context, snapshot) {
      return Container();
    },
  );
}

Repeated rebuilds can cause the Future to be recreated and the request to run repeatedly. Flutter's official fetch-data recipe recommends storing the Future in state and initializing it in initState() rather than repeatedly creating it in build(). :contentReference[oaicite:2]{index=2}


Better:


late Future futureUser;

 


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




25. Complete FutureBuilder API Example


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

 


class User {
  final int id;
  final String name;
  final String email;


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


  factory User.fromJson(Map json) {
    return User(
      id: json['id'],
      name: json['name'],
      email: json['email'],
    );
  }
}


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


  if (response.statusCode == 200) {
    return User.fromJson(
      jsonDecode(response.body) as Map,
    );
  }


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


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


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


class _UserScreenState extends State {
  late Future futureUser;


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


  void retry() {
    setState(() {
      futureUser = fetchUser();
    });
  }


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


          if (snapshot.hasError) {
            return Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  const Text('Unable to load user'),
                  const SizedBox(height: 12),
                  ElevatedButton(
                    onPressed: retry,
                    child: const Text('Retry'),
                  ),
                ],
              ),
            );
          }


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


          final user = snapshot.data!;


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




26. Handling API Errors in POST Requests


Error handling is also important when sending form data to an API.


Future createUser(String name, String email) async {
  try {
    final response = await http.post(
      Uri.parse('https://example.com/users'),
      headers: {
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode({
        'name': name,
        'email': email,
      }),
    );

 


    if (response.statusCode == 201) {
      print('User created successfully');
    } else {
      throw Exception(
        'Failed to create user: ${response.statusCode}',
      );
    }
  } catch (e) {
    print('Error: $e');
  }
}


Flutter's official POST example checks the expected success status code and throws an exception when the response does not meet the expected condition. :contentReference[oaicite:3]{index=3}




27. Form Submission Loading


When submitting a form, disable the submit button while the request is running.


bool isSubmitting = false;

 


Future submit() async {
  if (isSubmitting) return;


  setState(() {
    isSubmitting = true;
  });


  try {
    await createUser('Rahul', '[email protected]');


    if (!mounted) return;


    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('User created successfully'),
      ),
    );
  } catch (e) {
    if (!mounted) return;


    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('Failed to create user'),
      ),
    );
  } finally {
    if (mounted) {
      setState(() {
        isSubmitting = false;
      });
    }
  }
}




28. Preventing Duplicate API Requests


Users may accidentally tap a button multiple times. Use a loading flag to prevent duplicate submissions.


if (isLoading) {
  return;
}

 


setState(() {
  isLoading = true;
});


Disable the button while the request is running:


ElevatedButton(
  onPressed: isLoading ? null : submit,
  child: Text(
    isLoading ? 'Please wait...' : 'Submit',
  ),
)



29. Displaying Errors with SnackBar


For short-lived errors, a SnackBar is useful.


ScaffoldMessenger.of(context).showSnackBar(
  const SnackBar(
    content: Text('Unable to connect to the server'),
  ),
);



30. Displaying Errors with a Dialog


For important errors that require user attention, a dialog can be used.


showDialog(
  context: context,
  builder: (context) {
    return AlertDialog(
      title: const Text('Error'),
      content: const Text(
        'Unable to complete the request.',
      ),
      actions: [
        TextButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: const Text('OK'),
        ),
      ],
    );
  },
);



31. Authentication Errors


APIs that require authentication may return 401 Unauthorized when a token is missing, invalid, or expired.


if (response.statusCode == 401) {
  throw Exception('Session expired. Please log in again.');
}

A production application can use this condition to redirect the user to the login screen or refresh credentials when appropriate.




32. Handling Server Errors


Server-side failures commonly use 5xx status codes.


if (response.statusCode >= 500) {
  throw Exception(
    'Server is temporarily unavailable. Please try again later.',
  );
}

The user should generally receive a simple and actionable message rather than raw server details.




33. Pull-to-Refresh


API-based lists often support pull-to-refresh using RefreshIndicator.


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

 


    await futureUsers;
  },
  child: ListView(
    children: const [
      ListTile(title: Text('User 1')),
      ListTile(title: Text('User 2')),
    ],
  ),
)




34. Keeping Existing Data While Refreshing


For better user experience, an application can keep previously loaded data visible while showing a small loading indicator during refresh instead of replacing the entire screen with a spinner.


Column(
  children: [
    if (isRefreshing)
      const LinearProgressIndicator(),

 


    Expanded(
      child: UserList(users: users),
    ),
  ],
)




35. Skeleton Loading


A skeleton loader displays placeholder shapes while content is being loaded.


Column(
  children: [
    Container(
      height: 20,
      width: 200,
      color: Colors.grey.shade300,
    ),
    const SizedBox(height: 10),
    Container(
      height: 15,
      width: double.infinity,
      color: Colors.grey.shade300,
    ),
  ],
)

Skeleton loading can provide a more realistic representation of the final screen than a single spinner.




36. Loading State for API Lists


if (isLoading) {
  return const Center(
    child: CircularProgressIndicator(),
  );
}

 


if (errorMessage != null) {
  return Center(
    child: Text(errorMessage!),
  );
}


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),
    );
  },
);




37. API State Management with an Enum


Instead of managing several independent Boolean variables, an application can define a single API state.


enum ApiStatus {
  initial,
  loading,
  success,
  empty,
  error,
}

Then maintain the current state:


ApiStatus status = ApiStatus.initial;

Update it while communicating with the API:


Future loadUsers() async {
  setState(() {
    status = ApiStatus.loading;
  });

 


  try {
    final result = await fetchUsers();


    if (!mounted) return;


    setState(() {
      users = result;
      status = result.isEmpty
          ? ApiStatus.empty
          : ApiStatus.success;
    });
  } catch (e) {
    if (!mounted) return;


    setState(() {
      status = ApiStatus.error;
    });
  }
}




38. Generic API Result Pattern


A reusable result class can represent success or failure.


class ApiResult {
  final T? data;
  final String? error;
  final bool isSuccess;

 


  ApiResult.success(this.data)
      : error = null,
        isSuccess = true;


  ApiResult.failure(this.error)
      : data = null,
        isSuccess = false;
}


This pattern can be useful in larger applications where many API services need consistent error handling.




39. Service Layer for API Handling


Instead of putting HTTP code directly inside widgets, create a separate API service.


class UserService {
  Future> fetchUsers() async {
    final response = await http.get(
      Uri.parse('https://example.com/users'),
    );

 


    if (response.statusCode != 200) {
      throw Exception('Failed to load users');
    }


    final data = jsonDecode(response.body) as List;


    return data
        .map((json) => User.fromJson(json))
        .toList();
  }
}


The UI can then call:


final service = UserService();

 


final users = await service.fetchUsers();




40. Repository Pattern


For larger applications, a repository can sit between the UI/state-management layer and API services.


UI
 ↓
State Management
 ↓
Repository
 ↓
API Service
 ↓
HTTP Client
 ↓
Backend API

This architecture makes it easier to change data sources, add caching, and test business logic independently from the UI.




41. Handling Large JSON Responses


Large JSON responses may require significant parsing work. Flutter's documentation explains that expensive JSON parsing can cause UI jank and can be moved to a separate isolate using tools such as compute(). :contentReference[oaicite:4]{index=4}


import 'package:flutter/foundation.dart';

 


List parseUsers(String responseBody) {
  final data = jsonDecode(responseBody) as List;


  return data
      .map((json) => User.fromJson(json))
      .toList();
}


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


  if (response.statusCode != 200) {
    throw Exception('Failed to load users');
  }


  return compute(parseUsers, response.body);
}




42. Error Handling Architecture


Flutter UI
   ↓
Loading / Success / Empty / Error
   ↓
State Management
   ↓
Repository
   ↓
API Service
   ↓
HTTP Request
   ↓
Backend API

Each layer should have a clear responsibility:










Layer Responsibility
UI Display loading, data, empty and error states
State Management Maintain current application/API state
Repository Coordinate data sources
API Service Perform HTTP requests
Model Represent structured API data
Backend Process requests and return responses



43. Common API Error Categories












Error Category Possible Cause Recommended UI
Network Error No internet or connection failure Connection message + retry
Timeout Server took too long Timeout message + retry
Authentication Expired or invalid credentials Login/refresh action
Authorization User lacks permission Access denied message
Not Found Resource does not exist Not found screen
Validation Invalid request data Field or form errors
Server Error Backend failure Try again later
Parsing Error Unexpected JSON Generic data error



44. Common Mistakes


Mistake 1: Calling API in build()


This can cause repeated API requests during rebuilds.


Mistake 2: No loading state


Users may think the application is frozen.


Mistake 3: No error state


Network failures may leave the screen blank.


Mistake 4: Ignoring status codes


A response should be validated before treating it as successful.


Mistake 5: Showing raw technical errors


Messages such as stack traces are not appropriate for normal users.


Mistake 6: Infinite loading


Always ensure failed requests transition into an error state instead of remaining indefinitely in loading.


Mistake 7: Duplicate requests


Disable buttons or otherwise guard against repeated submissions.


Mistake 8: Updating disposed widgets


Use mounted checks when appropriate after asynchronous operations.




45. Best Practices for API Loading and Error Handling



  • Always show a clear loading state.

  • Check HTTP status codes.

  • Use model classes for structured responses.

  • Handle network exceptions.

  • Handle timeout errors.

  • Handle JSON parsing errors.

  • Separate empty states from error states.

  • Provide a retry action when appropriate.

  • Avoid API calls directly inside build().

  • Prevent duplicate form submissions.

  • Use user-friendly error messages.

  • Keep API code separate from UI code.

  • Use repositories or services for larger projects.

  • Use background parsing for expensive JSON processing.

  • Do not expose sensitive technical information to users.




46. Practical Mini Project: User API Screen


Build a Flutter application with the following requirements:



  1. Fetch users from an API.

  1. Display a loading indicator while fetching.

  1. Display users after a successful response.

  1. Display an empty state when no users exist.

  1. Display an error message when the request fails.

  1. Add a Retry button.

  1. Add pull-to-refresh.

  1. Handle timeout errors.

  1. Display user details in a separate screen.


Suggested API Flow


User opens screen
      ↓
Set state = Loading
      ↓
Call API
      ↓
Check HTTP status
      ↓
Decode JSON
      ↓
Convert JSON → User objects
      ↓
Is list empty?
   ↙          ↘
 Yes           No
 ↓             ↓
Empty UI     Success UI
      ↘       ↙
       Error if request fails



47. Interview Questions



  1. What is API loading in Flutter?

  1. Why is loading state important?

  1. How do you handle API errors in Flutter?

  1. What is FutureBuilder?

  1. What is ConnectionState.waiting?

  1. How do you check an HTTP response status code?

  1. What is the difference between an empty state and an error state?

  1. How do you handle network exceptions?

  1. How can you implement a Retry button?

  1. Why should API calls generally not be placed directly inside build()?

  1. How can you prevent duplicate API requests?

  1. What is the purpose of mounted?

  1. How can you handle API timeout errors?

  1. How do you handle malformed JSON?

  1. What is a custom API exception?

  1. How can API loading states be represented using an enum?

  1. Why should API services be separated from UI code?

  1. When should JSON parsing be moved to another isolate?




48. Quick Revision















Concept Key Point
API Loading Show progress while waiting for the server
Future Represents an asynchronous result
FutureBuilder Builds UI according to Future state
HTTP Status Determines whether a request succeeded or failed
Exception Represents an operation failure
Empty State Successful request with no useful records
Retry Allows the user to repeat a failed request
Timeout Prevents a request from waiting indefinitely
mounted Helps verify that a State object is still active
Service Layer Keeps API communication separate from UI
Repository Coordinates data sources in larger applications



49. Useful Resources











50. Learn Flutter with JustAcademy


For structured Flutter learning, practical development, and course guidance, explore the following resources:


JustAcademy Flutter Training Course


Register for Flutter Course Demo




Conclusion


API loading and error handling are fundamental skills for building reliable Flutter applications. A professional API screen should clearly manage the complete request lifecycle: initial state, loading, success, empty data, and error. By combining the http package, proper HTTP status-code checks, JSON parsing, model classes, FutureBuilder, exception handling, retry mechanisms, timeout handling, and clean service architecture, developers can create Flutter applications that remain understandable and responsive even when network requests fail.

 

whatsapp