Flutter HTTP Requests and API Integration: A Practical Guide
When building Flutter apps, you'll almost always need to fetch data from the internet. Whether you're loading user profiles, fetching news articles, or syncing data with a backend server, understanding how to make HTTP requests is essential. In this article, we'll explore how to work with HTTP requests in Flutter, from basic GET requests to handling complex API integrations.
Understanding HTTP in Flutter
HTTP (Hypertext Transfer Protocol) is the foundation of communication between your Flutter app and web servers. Flutter provides excellent support for making HTTP requests through the http package, which is maintained by the Dart team and offers a simple, powerful API for network operations.
Before we dive into code, let's understand the basic HTTP methods you'll use most often:
- GET: Retrieve data from a server
- POST: Send data to create a new resource
- PUT: Update an existing resource
- DELETE: Remove a resource
Here's how an HTTP request flows between your Flutter app and a server:
Setting Up the HTTP Package
First, add the http package to your pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
http: ^1.1.0
Then run flutter pub get to install the package. Now you're ready to make HTTP requests!
Making Your First GET Request
Let's start with a simple example of fetching data from a public API. Here's how you make a GET request:
import 'package:http/http.dart' as http;
import 'dart:convert';
Future fetchUserData() async {
final url = Uri.parse('https://api.example.com/users/1');
try {
final response = await http.get(url);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
print('User data: $data');
} else {
print('Failed to load user: ${response.statusCode}');
}
} catch (e) {
print('Error fetching user: $e');
}
}
Let's break down what's happening here:
- We import the
httppackage and Dart'sconvertlibrary for JSON parsing - We create a
Uriobject from our API endpoint - We use
awaitto wait for the HTTP request to complete - We check the status code (200 means success)
- We parse the JSON response using
jsonDecode
Working with Models and JSON
In real applications, you'll want to convert JSON responses into Dart objects. Let's create a model class and see how to parse API responses properly:
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'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
}
Map toJson() {
return {
'id': id,
'name': name,
'email': email,
};
}
}
Now let's update our fetch function to use this model:
Future fetchUser(int userId) async {
final url = Uri.parse('https://api.example.com/users/$userId');
try {
final response = await http.get(url);
if (response.statusCode == 200) {
final jsonData = jsonDecode(response.body) as Map;
return User.fromJson(jsonData);
} else {
print('Failed to load user: ${response.statusCode}');
return null;
}
} catch (e) {
print('Error fetching user: $e');
return null;
}
}
Making POST Requests
When you need to send data to a server, you'll use POST requests. Here's how to create a new user:
Future createUser(String name, String email) async {
final url = Uri.parse('https://api.example.com/users');
final body = jsonEncode({
'name': name,
'email': email,
});
try {
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: body,
);
if (response.statusCode == 201) {
final jsonData = jsonDecode(response.body) as Map;
return User.fromJson(jsonData);
} else {
print('Failed to create user: ${response.statusCode}');
return null;
}
} catch (e) {
print('Error creating user: $e');
return null;
}
}
Notice that we're setting the Content-Type header to tell the server we're sending JSON data. This is important for APIs that expect JSON format.
Handling Headers and Authentication
Many APIs require authentication tokens or custom headers. Here's how to include them in your requests:
Future fetchAuthenticatedUser(String token) async {
final url = Uri.parse('https://api.example.com/users/me');
final headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
};
try {
final response = await http.get(url, headers: headers);
if (response.statusCode == 200) {
final jsonData = jsonDecode(response.body) as Map;
return User.fromJson(jsonData);
} else if (response.statusCode == 401) {
print('Unauthorized - token may be invalid');
return null;
} else {
print('Failed to load user: ${response.statusCode}');
return null;
}
} catch (e) {
print('Error fetching user: $e');
return null;
}
}
Error Handling Best Practices
Robust error handling is crucial for network requests. Here's a more comprehensive approach:
Proper error handling ensures your app gracefully handles various failure scenarios:
class ApiException implements Exception {
final String message;
final int? statusCode;
ApiException(this.message, [this.statusCode]);
@override
String toString() => message;
}
Future fetchUserWithErrorHandling(int userId) async {
final url = Uri.parse('https://api.example.com/users/$userId');
try {
final response = await http.get(url);
if (response.statusCode == 200) {
final jsonData = jsonDecode(response.body) as Map;
return User.fromJson(jsonData);
} else if (response.statusCode == 404) {
throw ApiException('User not found', 404);
} else if (response.statusCode == 500) {
throw ApiException('Server error', 500);
} else {
throw ApiException('Failed to load user: ${response.statusCode}', response.statusCode);
}
} on SocketException {
throw ApiException('No internet connection');
} on FormatException {
throw ApiException('Invalid response format');
} catch (e) {
if (e is ApiException) rethrow;
throw ApiException('Unexpected error: $e');
}
}
Note: Don't forget to import dart:io for SocketException:
import 'dart:io';
Creating a Reusable API Service
As your app grows, you'll want to centralize your API calls. Here's a simple service class pattern:
Using a centralized API service helps organize your code and makes it easier to manage authentication and error handling:
class ApiService {
final String baseUrl;
final Map defaultHeaders;
ApiService({
required this.baseUrl,
String? authToken,
}) : defaultHeaders = {
'Content-Type': 'application/json',
if (authToken != null) 'Authorization': 'Bearer $authToken',
};
Future
Now you can use this service throughout your app:
final apiService = ApiService(
baseUrl: 'https://api.example.com',
authToken: 'your-token-here',
);
final userData = await apiService.get('/users/1');
final user = User.fromJson(userData);
Working with Lists and Pagination
Many APIs return lists of data. Here's how to handle that:
Future> fetchUsers({int page = 1, int limit = 10}) async {
final url = Uri.parse(
'https://api.example.com/users?page=$page&limit=$limit'
);
try {
final response = await http.get(url);
if (response.statusCode == 200) {
final jsonData = jsonDecode(response.body);
final List usersList = jsonData['data'] as List;
return usersList.map((json) => User.fromJson(json as Map)).toList();
} else {
throw ApiException('Failed to load users: ${response.statusCode}', response.statusCode);
}
} catch (e) {
if (e is ApiException) rethrow;
throw ApiException('Error fetching users: $e');
}
}
Integrating with Flutter Widgets
Now let's see how to use HTTP requests in a Flutter widget. We'll use FutureBuilder to handle asynchronous data loading:
class UserProfileWidget extends StatefulWidget {
final int userId;
const UserProfileWidget({Key? key, required this.userId}) : super(key: key);
@override
State createState() => _UserProfileWidgetState();
}
class _UserProfileWidgetState extends State {
late Future _userFuture;
@override
void initState() {
super.initState();
_userFuture = fetchUser(widget.userId);
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
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) {
return const Center(child: Text('No user data'));
}
final user = snapshot.data!;
return Column(
children: [
Text('Name: ${user.name}'),
Text('Email: ${user.email}'),
],
);
},
);
}
}
Timeouts and Cancellation
Network requests can sometimes hang indefinitely. It's important to set timeouts and handle cancellation:
Future fetchUserWithTimeout(int userId) async {
final url = Uri.parse('https://api.example.com/users/$userId');
try {
final response = await http.get(url).timeout(
const Duration(seconds: 10),
onTimeout: () {
throw ApiException('Request timeout');
},
);
if (response.statusCode == 200) {
final jsonData = jsonDecode(response.body) as Map;
return User.fromJson(jsonData);
} else {
throw ApiException('Failed to load user: ${response.statusCode}', response.statusCode);
}
} catch (e) {
if (e is ApiException) rethrow;
throw ApiException('Error fetching user: $e');
}
}
Best Practices and Tips
Here are some important tips for working with HTTP requests in Flutter:
- Always handle errors: Network requests can fail for many reasons. Always wrap them in try-catch blocks.
- Use models: Convert JSON to Dart objects using model classes. This makes your code more maintainable and type-safe.
- Set timeouts: Prevent your app from hanging on slow networks by setting reasonable timeouts.
- Handle loading states: Show loading indicators while requests are in progress.
- Cache when appropriate: Consider caching API responses to reduce network usage and improve performance.
- Use HTTPS: Always use secure connections in production apps.
- Validate responses: Don't assume API responses are always valid. Check for null values and validate data structure.
Conclusion
Making HTTP requests is a fundamental skill for Flutter developers. By understanding how to use the http package, handle errors properly, and integrate network calls with your widgets, you'll be able to build apps that communicate effectively with backend services. Remember to always handle errors gracefully, use proper models for your data, and provide good user feedback during network operations.
As you continue building Flutter apps, you might want to explore more advanced topics like interceptors, request cancellation, and state management solutions that work well with network requests. But with the basics covered here, you're well-equipped to start integrating APIs into your Flutter applications!