Network operations are a fundamental part of modern mobile applications. In Flutter, handling network requests requires robust error handling to provide a seamless user experience, even when things go wrong. Comprehensive error handling includes managing various types of exceptions, displaying user-friendly error messages, and implementing retry mechanisms.
Why Comprehensive Error Handling?
- Improved User Experience: Provides clear and helpful error messages instead of crashing or displaying technical jargon.
- Robustness: Ensures the application gracefully handles unexpected issues like network outages or server errors.
- Maintainability: Centralized error handling makes the codebase easier to maintain and debug.
Common Network Errors in Flutter
Before diving into the implementation, let’s identify the common network-related errors you might encounter:
- TimeoutException: The request took too long to complete.
- SocketException: A low-level network issue (e.g., no internet connection).
- HttpException: General HTTP-related exception.
- FormatException: Incorrect data format received (e.g., invalid JSON).
- ServerErrorException: Errors that can came from the server that the user cannot handle directly from its app like 500 internal error
Implementing Comprehensive Error Handling
Step 1: Setup Dependencies
Make sure you have the http package added to your pubspec.yaml file.
dependencies:
flutter:
sdk: flutter
http: ^0.13.5
Run flutter pub get to install the dependencies.
Step 2: Implement a Generic Network Request Function
Create a generic function to handle network requests with comprehensive error handling.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
class NetworkService {
static const int timeoutDuration = 10; // seconds
Future<dynamic> get(String url) async {
return _handleRequest(() => http.get(Uri.parse(url)).timeout(const Duration(seconds: timeoutDuration)));
}
Future<dynamic> post(String url, {dynamic body}) async {
return _handleRequest(() => http.post(Uri.parse(url), body: body).timeout(const Duration(seconds: timeoutDuration)));
}
Future<dynamic> put(String url, {dynamic body}) async {
return _handleRequest(() => http.put(Uri.parse(url), body: body).timeout(const Duration(seconds: timeoutDuration)));
}
Future<dynamic> delete(String url) async {
return _handleRequest(() => http.delete(Uri.parse(url)).timeout(const Duration(seconds: timeoutDuration)));
}
Future<dynamic> _handleRequest(Future<http.Response> Function() request) async {
try {
final response = await request();
return _response(response);
} on SocketException {
throw FetchDataException('No Internet connection');
} on TimeoutException {
throw ApiNotRespondingException('API not responding');
}
}
dynamic _response(http.Response response) {
switch (response.statusCode) {
case 200:
var responseJson = json.decode(response.body.toString());
print(responseJson);
return responseJson;
case 400:
throw BadRequestException(response.body.toString());
case 401:
case 403:
throw UnauthorisedException(response.body.toString());
case 500:
default:
throw FetchDataException(
'Error occurred while Communication with Server with StatusCode : ${response.statusCode}');
}
}
}
class CustomException implements Exception {
final _message;
final _prefix;
CustomException([this._message, this._prefix]);
String toString() {
return "$_prefix$_message";
}
}
class FetchDataException extends CustomException {
FetchDataException([String? message])
: super(message, "Error During Communication: ");
}
class BadRequestException extends CustomException {
BadRequestException([message]) : super(message, "Invalid Request: ");
}
class UnauthorisedException extends CustomException {
UnauthorisedException([message]) : super(message, "Unauthorised: ");
}
class InvalidInputException extends CustomException {
InvalidInputException([String? message]) : super(message, "Invalid Input: ");
}
class ApiNotRespondingException extends CustomException {
ApiNotRespondingException([String? message]) : super(message, "Api not responding: ");
}
Explanation:
- Generic Methods: This code sets up a `NetworkService` class with generic methods (`get`, `post`, `put`, `delete`) to handle common HTTP requests.
- Error Handling: Uses a central `_handleRequest` function to manage network requests. It catches common exceptions like `SocketException` (no internet) and `TimeoutException`, throwing custom exceptions for each case.
- Custom Exceptions: Defines a set of custom exception classes (`FetchDataException`, `BadRequestException`, `UnauthorisedException`, `InvalidInputException`, `ApiNotRespondingException`) for specific error scenarios, inheriting from a base `CustomException`. This makes error handling more specific and easier to manage.
- Response Handling: The `_response` method processes the HTTP response. It checks the status code and throws appropriate exceptions for client errors (400, 401, 403) and server errors (500 or any other unexpected status code). A status code of 200 (OK) returns the JSON-decoded response body.
- Timeout: Configures a `timeoutDuration` for requests, set to 10 seconds. Each request is wrapped in a `timeout` function to prevent indefinite waiting.
Step 3: Using the Network Service in your App
Here’s how you might use the NetworkService class in your Flutter app, along with UI updates to show errors:
import 'package:flutter/material.dart';
import 'network_service.dart'; // Assuming the above code is in 'network_service.dart'
class ExampleScreen extends StatefulWidget {
@override
_ExampleScreenState createState() => _ExampleScreenState();
}
class _ExampleScreenState extends State<ExampleScreen> {
final NetworkService networkService = NetworkService();
String data = 'No data yet.';
String errorMessage = '';
Future<void> fetchData() async {
setState(() {
errorMessage = ''; // Clear any previous errors
});
try {
final response = await networkService.get('https://jsonplaceholder.typicode.com/todos/1');
setState(() {
data = response['title']; // Access the 'title' from the JSON response
});
} catch (e) {
setState(() {
errorMessage = e.toString(); // Set the error message from the exception
data = 'Error fetching data.'; // Reset or indicate data state on error
});
print('Error fetching data: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Network Service Example')),
body: Padding(
padding: const EdgeInsets(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Data: $data', style: TextStyle(fontSize: 16)),
SizedBox(height: 10),
if (errorMessage.isNotEmpty) // Only show error message if it exists
Text('Error: $errorMessage', style: TextStyle(color: Colors.red)),
ElevatedButton(
onPressed: fetchData,
child: Text('Fetch Data'),
),
],
),
),
);
}
}
Key improvements:
- State Management with Error Display: Manages state for data, displaying it and any error messages on the screen. Utilizes `setState` to rebuild the UI with updated information.
- Error Handling in UI: Catches exceptions during data fetching, sets an error message in the state, and displays this message in the UI, offering clear feedback to the user.
- Conditional Error Display: Only shows the error message UI if there’s an error, avoiding empty or misleading displays.
- Asynchronous Data Handling: Uses `async` and `await` to properly handle the asynchronous `fetchData` function, ensuring the UI updates smoothly and doesn’t block.
Additional Best Practices
- Implement Retry Logic: For transient errors (e.g., timeouts), consider implementing a retry mechanism with exponential backoff.
- Centralized Error Reporting: Integrate error reporting tools (e.g., Firebase Crashlytics) to monitor and address issues in production.
- Provide Contextual Messages: Tailor error messages to be informative and actionable for the user.
Conclusion
Implementing comprehensive error handling for network issues in Flutter is crucial for creating a reliable and user-friendly application. By anticipating and handling various network errors, providing informative feedback, and using best practices like retry logic, you can ensure a seamless experience for your users, even when faced with network challenges. This comprehensive approach not only enhances user satisfaction but also makes your application more robust and easier to maintain.