Flutter apps communicate with backend services via HTTP APIs. The http package is the standard way to make requests.
HTTP Package
YAML
dependencies:
http: ^1.1.0
dio: ^5.4.0 # Alternative with interceptorsDart
import 'dart:convert';
import 'package:http/http.dart' as http;
class ApiService {
static const String baseUrl = 'https://api.example.com';
// GET request
static Future<List<User>> getUsers() async {
final response = await http.get(
Uri.parse('$baseUrl/users'),
headers: {'Authorization': 'Bearer $token'},
);
if (response.statusCode == 200) {
final List data = jsonDecode(response.body);
return data.map((json) => User.fromJson(json)).toList();
} else {
throw ApiException('Failed to load users: ${response.statusCode}');
}
}
// POST request
static Future<User> createUser(Map<String, dynamic> data) async {
final response = await http.post(
Uri.parse('$baseUrl/users'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
},
body: jsonEncode(data),
);
if (response.statusCode == 201) {
return User.fromJson(jsonDecode(response.body));
} else {
throw ApiException('Failed to create user');
}
}
// PUT request
static Future<User> updateUser(String id, Map<String, dynamic> data) async {
final response = await http.put(
Uri.parse('$baseUrl/users/$id'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
},
body: jsonEncode(data),
);
if (response.statusCode == 200) {
return User.fromJson(jsonDecode(response.body));
} else {
throw ApiException('Failed to update user');
}
}
// DELETE request
static Future<void> deleteUser(String id) async {
final response = await http.delete(
Uri.parse('$baseUrl/users/$id'),
headers: {'Authorization': 'Bearer $token'},
);
if (response.statusCode != 204) {
throw ApiException('Failed to delete user');
}
}
}
class ApiException implements Exception {
final String message;
ApiException(this.message);
@override
String toString() => 'ApiException: $message';
}Data Models with Serialization
Dart
class User {
final String id;
final String name;
final String email;
final DateTime createdAt;
User({
required this.id,
required this.name,
required this.email,
required this.createdAt,
});
// JSON → Object
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
email: json['email'],
createdAt: DateTime.parse(json['created_at']),
);
}
// Object → JSON
Map<String, dynamic> toJson() {
return {
'name': name,
'email': email,
};
}
// Copy with modifications
User copyWith({String? name, String? email}) {
return User(
id: id,
name: name ?? this.name,
email: email ?? this.email,
createdAt: createdAt,
);
}
}Loading States
Dart
class UsersPage extends StatefulWidget {
const UsersPage({super.key});
@override
State<UsersPage> createState() => _UsersPageState();
}
class _UsersPageState extends State<UsersPage> {
List<User> _users = [];
bool _loading = true;
String? _error;
@override
void initState() {
super.initState();
_loadUsers();
}
Future<void> _loadUsers() async {
setState(() {
_loading = true;
_error = null;
});
try {
final users = await ApiService.getUsers();
setState(() {
_users = users;
_loading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_loading = false;
});
}
}
@override
Widget build(BuildContext context) {
if (_loading) return const Center(child: CircularProgressIndicator());
if (_error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: $_error'),
ElevatedButton(onPressed: _loadUsers, child: Text('Retry')),
],
),
);
}
return RefreshIndicator(
onRefresh: _loadUsers,
child: ListView.builder(
itemCount: _users.length,
itemBuilder: (context, index) {
final user = _users[index];
return ListTile(
leading: CircleAvatar(child: Text(user.name[0])),
title: Text(user.name),
subtitle: Text(user.email),
);
},
),
);
}
}Dio (Advanced HTTP Client)
Dart
import 'package:dio/dio.dart';
final dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 5),
receiveTimeout: Duration(seconds: 3),
headers: {'Authorization': 'Bearer $token'},
));
// Interceptor for logging
dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
// Error handling interceptor
dio.interceptors.add(InterceptorsWrapper(
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
// Refresh token
final newToken = await refreshAuthToken();
error.requestOptions.headers['Authorization'] = 'Bearer $newToken';
// Retry the request
final response = await dio.fetch(error.requestOptions);
return handler.resolve(response);
}
handler.next(error);
},
));
// Usage
final response = await dio.get('/users');💡 Tip: Always handle network errors gracefully. Show a retry button, cache previous data, and never assume the network is available.
Next: Testing in Flutter