← Back to Articles

Flutter HTTP Requests and API Integration: A Practical Guide

Flutter HTTP Requests and API Integration: A Practical Guide

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:

Flutter App API Server HTTP Request HTTP Response

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 http package and Dart's convert library for JSON parsing
  • We create a Uri object from our API endpoint
  • We use await to 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:

HTTP Request Success 200 OK Not Found 404 Error Server Error 500 Error Network Error No Connection Timeout Request Timeout Error Handler

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:

ApiService Widget A Widget B Widget C API Server

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> get(String endpoint) async {
    final url = Uri.parse('$baseUrl$endpoint');
    final response = await http.get(url, headers: defaultHeaders);
    return _handleResponse(response);
  }
  
  Future> post(String endpoint, Map data) async {
    final url = Uri.parse('$baseUrl$endpoint');
    final response = await http.post(
      url,
      headers: defaultHeaders,
      body: jsonEncode(data),
    );
    return _handleResponse(response);
  }
  
  Map _handleResponse(http.Response response) {
    if (response.statusCode >= 200 && response.statusCode < 300) {
      return jsonDecode(response.body) as Map;
    } else {
      throw ApiException(
        'Request failed with status: ${response.statusCode}',
        response.statusCode,
      );
    }
  }
}

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!