Home
Dart
OOP
Flutter
Contact

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 http package for GET, POST, PUT, DELETE requests
  • Create fromJson/toJson factory constructors for type safety
  • Handle errors with custom ApiException classes
  • Use Future.wait for parallel requests
🚀 Next Step Continue to Performance Optimization.
K
Kesavaraja Murugesan
Flutter Developer & Educator

Flutter developer with 3 years of experience building production-grade mobile applications. Passionate about teaching clean architecture patterns and helping developers write maintainable, scalable code.

← Local Storage Next: Best Practices →