API Integration & JSON Parsing
Make HTTP requests, handle responses, and transform raw JSON into type-safe Dart objects.
Error Handling Patterns
Production networking code must handle every failure gracefully. Here's a robust pattern:
network_error_handling.dart
class ApiResult<T> {
final T? data;
final String? error;
final int statusCode;
ApiResult({this.data, this.error, this.statusCode = 200});
bool get isSuccess => error == null;
}
class ApiClient {
Future<ApiResult<List<Post>>> fetchPosts() async {
try {
final response = await http.get(
Uri.parse('https://api.example.com/posts'),
headers: {'Accept': 'application/json'},
).timeout(const Duration(seconds: 10));
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as List;
final posts = json.map((e) => Post.fromJson(e)).toList();
return ApiResult(data: posts);
}
return ApiResult(error: 'Server error');
} on TimeoutException {
return ApiResult(error: 'Connection timed out');
} on SocketException {
return ApiResult(error: 'No internet connection');
} catch (e) {
return ApiResult(error: 'Unexpected error: $e');
}
}
}
⚠️ Common Mistake: Not Closing HTTP Clients
Always call
client.close() when you're done, or use the http package's top-level functions which handle this automatically. Failing to close clients causes memory leaks.
HTTP Basics
Flutter uses the http package for network requests:
pubspec.yaml
dependencies:
http: ^1.2.0
api_client.dart
import 'package:http/http.dart' as http;
class ApiClient {
static const String baseUrl = 'https://api.example.com/v1';
static Future<Map<String, dynamic>> get(String endpoint) async {
final response = await http.get(
Uri.parse('$baseUrl/$endpoint'),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw ApiException(response.statusCode, response.body);
}
}
static Future<Map<String, dynamic>> post(String endpoint, Map<String, dynamic> body) async {
final response = await http.post(
Uri.parse('$baseUrl/$endpoint'),
headers: {'Content-Type': 'application/json'},
body: json.encode(body),
);
if (response.statusCode == 201 || response.statusCode == 200) {
return json.decode(response.body);
}
throw ApiException(response.statusCode, response.body);
}
}
JSON → Dart Objects
Create type-safe models with fromJson and toJson factories:
article.dart
class Article {
final String id;
final String title;
final String content;
final String author;
final DateTime publishedAt;
final List<String> tags;
const Article({
required this.id,
required this.title,
required this.content,
required this.author,
required this.publishedAt,
this.tags = const [],
});
// Factory constructor from JSON
factory Article.fromJson(Map<String, dynamic> json) {
return Article(
id: json['id'] as String,
title: json['title'] as String,
content: json['content'] as String,
author: json['author'] as String,
publishedAt: DateTime.parse(json['published_at']),
tags: List<String>.from(json['tags'] ?? []),
);
}
// Convert to JSON for POST requests
Map<String, dynamic> toJson() => {
'title': title,
'content': content,
'author': author,
'tags': tags,
};
}
Error Handling
api_exception.dart
class ApiException implements Exception {
final int statusCode;
final String message;
ApiException(this.statusCode, this.message);
String toString() => 'ApiException($statusCode): $message';
String get userMessage {
switch (statusCode) {
case 401: return 'Authentication required';
case 404: return 'Resource not found';
case 500: return 'Server error. Try again later.';
default: return 'Request failed ($statusCode)';
}
}
}
// Usage with try-catch
try {
var articles = await ApiClient.get('articles');
} on ApiException catch (e) {
print(e.userMessage);
} on Exception catch (e) {
print('Network error: $e');
}
Async Patterns
async_patterns.dart
// Sequential requests
var user = await ApiClient.get('user/1');
var posts = await ApiClient.get('user/1/posts');
// Parallel requests (faster)
var results = await Future.wait([
ApiClient.get('user/1'),
ApiClient.get('user/1/posts'),
ApiClient.get('user/1/bookmarks'),
]);
var [user, posts, bookmarks] = results;
// Stream-based (real-time updates)
Stream<List<Article>> watchArticles() async* {
while (true) {
var data = await ApiClient.get('articles');
var articles = (data['articles'] as List)
.map((j) => Article.fromJson(j)).toList();
yield articles;
await Future.delayed(const Duration(seconds: 30));
}
}
Real-World Example: Content Stream App
article_screen.dart
class ArticleScreen extends StatefulWidget {
const ArticleScreen({super.key});
@override
State<ArticleScreen> createState() => _ArticleScreenState();
}
class _ArticleScreenState extends State<ArticleScreen> {
List<Article> _articles = [];
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_loadArticles();
}
Future<void> _loadArticles() async {
try {
var data = await ApiClient.get('articles');
setState(() {
_articles = (data['articles'] as List)
.map((j) => Article.fromJson(j)).toList();
_isLoading = false;
});
} on ApiException catch (e) {
setState(() { _error = e.userMessage; _isLoading = false; });
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) return const CircularProgressIndicator();
if (_error != null) return Text(_error!);
return ListView.builder(
itemCount: _articles.length,
itemBuilder: (context, index) {
var article = _articles[index];
return ListTile(
title: Text(article.title),
subtitle: Text('By ${article.author}'),
trailing: Text(article.tags.join(', ')),
);
},
);
}
}
Summary
- Use the
httppackage for GET, POST, PUT, DELETE requests - Create
fromJson/toJsonfactory constructors for type safety - Handle errors with custom
ApiExceptionclasses - Use
Future.waitfor parallel requests
🚀 Next Step Continue to Performance Optimization.