← Back to Articles

Flutter Networking: HTTP Requests and API Integration

Flutter Networking: HTTP Requests and API Integration

Flutter Networking: HTTP Requests and API Integration

Building modern mobile apps almost always involves communicating with servers, fetching data from APIs, and sending information back. Whether you're loading user profiles, fetching news articles, or syncing data, understanding how to make HTTP requests in Flutter is essential.

In this article, we'll explore how to work with HTTP requests in Flutter, from basic GET and POST requests to handling responses, errors, and best practices for API integration. By the end, you'll be comfortable making network calls and integrating external data into your Flutter apps.

Setting Up HTTP Support

Flutter doesn't include HTTP functionality out of the box. You'll need to add the http package to your project. This is a popular, well-maintained package that provides a simple API for making HTTP requests.

First, add the dependency to your pubspec.yaml file:


dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0

Then run flutter pub get to install the package. Once that's done, you can import it in your Dart files:


import 'package:http/http.dart' as http;
import 'dart:convert';

The dart:convert library helps you work with JSON data, which is the most common format for API responses.

Making Your First GET Request

The simplest type of HTTP request is a GET request, which retrieves data from a server. Let's start with a basic example that fetches data from a public API:


Future fetchUserData() async {
  final url = Uri.parse('https://jsonplaceholder.typicode.com/users/1');
  
  try {
    final response = await http.get(url);
    
    if (response.statusCode == 200) {
      final jsonData = json.decode(response.body);
      print('User name: ${jsonData['name']}');
    } else {
      print('Failed to load user: ${response.statusCode}');
    }
  } catch (e) {
    print('Error fetching user: $e');
  }
}

Let's break down what's happening here:

  • Uri.parse() converts the URL string into a Uri object that the http package can use
  • http.get() makes an asynchronous GET request to the specified URL
  • response.statusCode tells us if the request was successful (200 means OK)
  • response.body contains the raw response data as a string
  • json.decode() parses the JSON string into a Dart Map or List

Notice that we're using async and await because HTTP requests are asynchronous operations. They take time to complete, and we don't want to block the UI thread while waiting for the response.

Working with JSON Responses

Most APIs return data in JSON format. Flutter makes it easy to work with JSON once you understand the basics. When you decode JSON, you get Dart objects:

  • JSON objects become Map<String, dynamic>
  • JSON arrays become List<dynamic>
  • JSON primitives (strings, numbers, booleans) become their Dart equivalents

Here's a more complete example that fetches a list of posts and displays them:


class Post {
  final int id;
  final String title;
  final String body;
  
  Post({required this.id, required this.title, required this.body});
  
  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }
}

Future<List<Post>> fetchPosts() async {
  final url = Uri.parse('https://jsonplaceholder.typicode.com/posts');
  final response = await http.get(url);
  
  if (response.statusCode == 200) {
    final List<dynamic> jsonList = json.decode(response.body);
    return jsonList.map((json) => Post.fromJson(json)).toList();
  } else {
    throw Exception('Failed to load posts');
  }
}

Creating a model class like Post with a fromJson factory constructor is a common pattern. It makes your code more type-safe and easier to work with. You can access properties like post.title instead of json['title'], which helps catch errors at compile time.

Sending POST Requests

When you need to send data to a server, you'll use POST requests. This is common when creating new resources, submitting forms, or updating data. Here's how to send a POST request with JSON data:


Future<Map<String, dynamic>> createPost(String title, String body) async {
  final url = Uri.parse('https://jsonplaceholder.typicode.com/posts');
  
  final response = await http.post(
    url,
    headers: {'Content-Type': 'application/json'},
    body: json.encode({
      'title': title,
      'body': body,
      'userId': 1,
    }),
  );
  
  if (response.statusCode == 201) {
    return json.decode(response.body);
  } else {
    throw Exception('Failed to create post');
  }
}

Key points about POST requests:

  • Set the Content-Type header to application/json when sending JSON
  • Use json.encode() to convert your Dart objects into a JSON string
  • Status code 201 typically means "Created" for successful POST requests
  • The response body usually contains the created resource with its assigned ID

Handling Errors Gracefully

Network requests can fail for many reasons: no internet connection, server errors, timeouts, or invalid URLs. Your app should handle these situations gracefully. Here's a robust error handling pattern:


Future<List<Post>> fetchPostsWithErrorHandling() async {
  try {
    final url = Uri.parse('https://jsonplaceholder.typicode.com/posts');
    final response = await http.get(url).timeout(
      const Duration(seconds: 10),
      onTimeout: () {
        throw TimeoutException('Request timed out');
      },
    );
    
    if (response.statusCode == 200) {
      final List<dynamic> jsonList = json.decode(response.body);
      return jsonList.map((json) => Post.fromJson(json)).toList();
    } else if (response.statusCode == 404) {
      throw Exception('Resource not found');
    } else if (response.statusCode >= 500) {
      throw Exception('Server error: ${response.statusCode}');
    } else {
      throw Exception('Failed to load posts: ${response.statusCode}');
    }
  } on SocketException {
    throw Exception('No internet connection');
  } on TimeoutException {
    throw Exception('Request timed out');
  } on FormatException {
    throw Exception('Invalid response format');
  } catch (e) {
    throw Exception('Unexpected error: $e');
  }
}

This example demonstrates several important error handling techniques:

  • timeout() prevents requests from hanging indefinitely
  • Checking specific status codes helps provide meaningful error messages
  • Catching specific exception types (SocketException, TimeoutException) allows for targeted error handling
  • A catch-all block handles unexpected errors

Integrating with State Management

In real Flutter apps, you'll typically want to display the fetched data in your UI and show loading or error states. Here's how you might integrate HTTP requests with a state management solution like Provider:


import 'package:flutter/foundation.dart';

class PostProvider with ChangeNotifier {
  List<Post> _posts = [];
  bool _isLoading = false;
  String? _error;
  
  List<Post> get posts => _posts;
  bool get isLoading => _isLoading;
  String? get error => _error;
  
  Future<void> loadPosts() async {
    _isLoading = true;
    _error = null;
    notifyListeners();
    
    try {
      _posts = await fetchPosts();
      _error = null;
    } catch (e) {
      _error = e.toString();
      _posts = [];
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

Then in your widget, you can use this provider to display the data:


class PostsScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<PostProvider>(
      builder: (context, postProvider, child) {
        if (postProvider.isLoading) {
          return Center(child: CircularProgressIndicator());
        }
        
        if (postProvider.error != null) {
          return Center(child: Text('Error: ${postProvider.error}'));
        }
        
        return ListView.builder(
          itemCount: postProvider.posts.length,
          itemBuilder: (context, index) {
            final post = postProvider.posts[index];
            return ListTile(
              title: Text(post.title),
              subtitle: Text(post.body),
            );
          },
        );
      },
    );
  }
}

This pattern separates your networking logic from your UI code, making both easier to test and maintain.

Adding Request Headers

Many APIs require authentication tokens or other headers. You can add headers to any request:


Future<Map<String, dynamic>> fetchAuthenticatedData(String token) async {
  final url = Uri.parse('https://api.example.com/user');
  
  final response = await http.get(
    url,
    headers: {
      'Authorization': 'Bearer $token',
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    },
  );
  
  if (response.statusCode == 200) {
    return json.decode(response.body);
  } else {
    throw Exception('Failed to load data');
  }
}

Common headers you might need:

  • Authorization: For API keys or bearer tokens
  • Content-Type: Specifies the format of data you're sending
  • Accept: Specifies the format you want to receive
  • User-Agent: Identifies your app to the server

Working with Query Parameters

Sometimes you need to add query parameters to your URLs, like pagination or filters. You can build URIs with query parameters easily:


Future<List<Post>> fetchPostsWithPagination(int page, int limit) async {
  final url = Uri.parse('https://jsonplaceholder.typicode.com/posts').replace(
    queryParameters: {
      '_page': page.toString(),
      '_limit': limit.toString(),
    },
  );
  
  final response = await http.get(url);
  
  if (response.statusCode == 200) {
    final List<dynamic> jsonList = json.decode(response.body);
    return jsonList.map((json) => Post.fromJson(json)).toList();
  } else {
    throw Exception('Failed to load posts');
  }
}

The replace() method on Uri lets you add or modify query parameters cleanly. The resulting URL will look like: https://jsonplaceholder.typicode.com/posts?_page=1&_limit=10

Best Practices

As you build more complex apps with networking, keep these best practices in mind:

1. Create a Service Class

Instead of scattering HTTP calls throughout your app, create a dedicated service class:


class ApiService {
  static const String baseUrl = 'https://api.example.com';
  
  final http.Client _client;
  
  ApiService({http.Client? client}) : _client = client ?? http.Client();
  
  Future<List<Post>> getPosts() async {
    final url = Uri.parse('$baseUrl/posts');
    final response = await _client.get(url);
    
    if (response.statusCode == 200) {
      final List<dynamic> jsonList = json.decode(response.body);
      return jsonList.map((json) => Post.fromJson(json)).toList();
    } else {
      throw Exception('Failed to load posts');
    }
  }
  
  Future<Post> createPost(Post post) async {
    final url = Uri.parse('$baseUrl/posts');
    final response = await _client.post(
      url,
      headers: {'Content-Type': 'application/json'},
      body: json.encode(post.toJson()),
    );
    
    if (response.statusCode == 201) {
      return Post.fromJson(json.decode(response.body));
    } else {
      throw Exception('Failed to create post');
    }
  }
}

This approach makes your code more organized, testable, and maintainable. You can easily mock the http.Client for testing.

2. Handle Network Connectivity

Check if the device has internet connectivity before making requests. The connectivity_plus package can help:


import 'package:connectivity_plus/connectivity_plus.dart';

Future<bool> hasInternetConnection() async {
  final connectivityResult = await Connectivity().checkConnectivity();
  return connectivityResult != ConnectivityResult.none;
}

3. Cache Responses When Appropriate

For data that doesn't change frequently, consider caching responses to reduce network usage and improve performance. Packages like flutter_cache_manager can help with this.

4. Use Interceptors for Common Logic

If you need to add authentication tokens or logging to all requests, consider using a package like dio which supports interceptors, or create a wrapper around the http client.

Alternative: Using Dio Package

While the http package is simple and sufficient for most needs, some developers prefer dio, which offers more features like interceptors, request cancellation, and better error handling:


import 'package:dio/dio.dart';

final dio = Dio(BaseOptions(
  baseUrl: 'https://api.example.com',
  headers: {'Content-Type': 'application/json'},
));

Future<List<Post>> fetchPostsWithDio() async {
  try {
    final response = await dio.get('/posts');
    final List<dynamic> jsonList = response.data;
    return jsonList.map((json) => Post.fromJson(json)).toList();
  } on DioException catch (e) {
    if (e.response != null) {
      throw Exception('Server error: ${e.response?.statusCode}');
    } else {
      throw Exception('Network error: ${e.message}');
    }
  }
}

Dio automatically parses JSON responses, which can make your code slightly cleaner. Choose the package that best fits your needs.

Conclusion

Making HTTP requests in Flutter is straightforward once you understand the basics. Remember to:

  • Handle errors gracefully with try-catch blocks
  • Use proper model classes for type safety
  • Show loading and error states in your UI
  • Organize your networking code in service classes
  • Consider connectivity and caching for better user experience

With these fundamentals, you're ready to integrate APIs into your Flutter apps. Start simple, add error handling early, and refactor as your app grows. Happy coding!

HTTP Request Flow Flutter App HTTP Client API Server Response Request and Response Structure HTTP Request Method: GET/POST URL: https://api.example.com Headers: Content-Type Body: JSON data HTTP Response Status: 200 OK Headers: Content-Type Body: JSON data Parse & Use