HTTP Package in Flutter

1 hour read This comprehensive article includes detailed explanations, comments, and advanced topics like SSL pinning, multipart requests, retries, timeouts, caching, and a RESTful API client implementation. Each example is explained step-by-step to ensure beginners can follow along easily. June 16, 2024 11:22 HTTP Package in Flutter HTTP Package in Flutter

HTTP Package in Flutter

Introduction

The http package in Flutter is a powerful tool that allows developers to make HTTP requests and handle responses seamlessly. Whether you need to fetch data from an API, send data to a server, or handle authentication, the http package has you covered. This article provides an in-depth look at the http package, teaching every feature included, and providing numerous examples to help you master HTTP in Flutter.

Setting Up

To get started, add the http package to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.3
        

Then, import it into your Dart file:

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

Features of the HTTP Package

  • Making GET, POST, PUT, DELETE requests
  • Handling headers and URL parameters
  • Session management
  • Token management for authentication
  • Basic and OAuth 2.0 authentication
  • Network interceptors for logging and modifying requests/responses
  • SSL pinning for enhanced security

Making HTTP Requests

GET Request

A GET request is used to retrieve data from a server. Here’s a basic example:

// Function to fetch data from an API using a GET request
Future fetchData() async {
  // Perform a GET request to the given URL
  final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));

  // Check if the request was successful
  if (response.statusCode == 200) {
    // If successful, print the response body
    print('Response data: ${response.body}');
  } else {
    // If not successful, print an error message
    print('Failed to load data');
  }
}
        

Explanation:

  • http.get: This method sends a GET request to the specified URL.
  • Uri.parse: Converts a string URL into a Uri object.
  • response.statusCode: Checks the status code of the response. A status code of 200 indicates success.
  • response.body: Contains the body of the response, typically in JSON format.

POST Request

A POST request is used to send data to a server. Here’s an example:

// Function to send data to an API using a POST request
Future sendData() async {
  // Perform a POST request to the given URL
  final response = await http.post(
    Uri.parse('https://jsonplaceholder.typicode.com/posts'),
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode({
      'title': 'Flutter POST Request',
      'body': 'This is a POST request example',
      'userId': '1',
    }),
  );

  // Check if the request was successful
  if (response.statusCode == 201) {
    // If successful, print the response body
    print('Response data: ${response.body}');
  } else {
    // If not successful, print an error message
    print('Failed to send data');
  }
}
        

Explanation:

  • http.post: This method sends a POST request to the specified URL.
  • headers: A map of headers to send with the request, specifying that the content type is JSON.
  • body: The data to send with the request, encoded as a JSON string.
  • jsonEncode: Converts a Dart object to a JSON string.

PUT Request

A PUT request is used to update data on a server. Here’s an example:

// Function to update data on an API using a PUT request
Future updateData() async {
  // Perform a PUT request to the given URL
  final response = await http.put(
    Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode({
      'title': 'Updated Title',
      'body': 'This is the updated body',
      'userId': '1',
    }),
  );

  // Check if the request was successful
  if (response.statusCode == 200) {
    // If successful, print the response body
    print('Response data: ${response.body}');
  } else {
    // If not successful, print an error message
    print('Failed to update data');
  }
}
        

Explanation:

  • http.put: This method sends a PUT request to the specified URL.
  • headers: A map of headers to send with the request, specifying that the content type is JSON.
  • body: The data to update, encoded as a JSON string.
  • jsonEncode: Converts a Dart object to a JSON string.

DELETE Request

A DELETE request is used to delete data from a server. Here’s an example:

// Function to delete data from an API using a DELETE request
Future deleteData() async {
  // Perform a DELETE request to the given URL
  final response = await http.delete(
    Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
  );

  // Check if the request was successful
  if (response.statusCode == 200) {
    // If successful, print a success message
    print('Data deleted successfully');
  } else {
    // If not successful, print an error message
    print('Failed to delete data');
  }
}
        

Explanation:

  • http.delete: This method sends a DELETE request to the specified URL.
  • response.statusCode: Checks the status code of the response. A status code of 200 indicates success.

Handling Headers and Parameters

Adding Headers

Headers can be added to HTTP requests to provide additional information to the server.

// Function to fetch data with custom headers
Future fetchDataWithHeaders() async {
  // Perform a GET request with

 custom headers
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
    headers: {
      'Authorization': 'Bearer YOUR_TOKEN',
    },
  );

  // Check if the request was successful
  if (response.statusCode == 200) {
    // If successful, print the response body
    print('Response data: ${response.body}');
  } else {
    // If not successful, print an error message
    print('Failed to load data');
  }
}
        

Explanation:

  • headers: A map of headers to send with the request. In this example, an Authorization header is added.

URL Parameters

URL parameters can be added to a GET request for filtering or sorting data.

// Function to fetch data with URL parameters
Future fetchDataWithParams() async {
  // Perform a GET request with URL parameters
  final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts?userId=1'));

  // Check if the request was successful
  if (response.statusCode == 200) {
    // If successful, print the response body
    print('Response data: ${response.body}');
  } else {
    // If not successful, print an error message
    print('Failed to load data');
  }
}
        

Explanation:

  • URL parameters are appended to the URL after a question mark (?). In this example, userId=1 is added to filter the posts by user ID.

Session Management

A session manager can help manage the state of the user's session across multiple requests.

// Class to manage session state
class SessionManager {
  Map _headers = {};

  // Method to set the authorization token
  void setToken(String token) {
    _headers['Authorization'] = 'Bearer $token';
  }

  // Method to perform a GET request with the session headers
  Future get(String url) async {
    return await http.get(Uri.parse(url), headers: _headers);
  }

  // Method to perform a POST request with the session headers
  Future post(String url, {Map? headers, dynamic body}) async {
    return await http.post(Uri.parse(url), headers: {..._headers, ...?headers}, body: body);
  }
}
        

Explanation:

  • _headers: A private map to store session headers.
  • setToken: A method to set the authorization token.
  • get: A method to perform a GET request with the session headers.
  • post: A method to perform a POST request with the session headers.

Token Management

Token management involves storing, retrieving, and refreshing tokens for authenticated requests.

// Class to manage authentication tokens
class TokenManager {
  String? _token;

  // Method to set the token
  void setToken(String token) {
    _token = token;
  }

  // Method to get the token
  String? getToken() {
    return _token;
  }

  // Method to clear the token
  void clearToken() {
    _token = null;
  }
}
        

Explanation:

  • _token: A private variable to store the token.
  • setToken: A method to set the token.
  • getToken: A method to retrieve the token.
  • clearToken: A method to clear the token.

Authentication

Basic Authentication

Basic authentication requires encoding the username and password in the request header.

// Function to fetch data with Basic Authentication
Future fetchDataWithBasicAuth() async {
  final username = 'user';
  final password = 'password';
  final credentials = base64Encode(utf8.encode('$username:$password'));

  final response = await http.get(
    Uri.parse('https://example.com/api'),
    headers: {
      'Authorization': 'Basic $credentials',
    },
  );

  if (response.statusCode == 200) {
    print('Response data: ${response.body}');
  } else {
    print('Failed to load data');
  }
}
        

Explanation:

  • base64Encode: Encodes the username and password in base64 format for Basic Authentication.
  • Authorization: A header to include the encoded credentials.

OAuth 2.0 Authentication

OAuth 2.0 is a more secure authentication method often used for API requests.

// Function to authenticate using OAuth 2.0 and fetch data
Future fetchDataWithOAuth() async {
  // Step 1: Obtain an access token
  final response = await http.post(
    Uri.parse('https://example.com/oauth/token'),
    body: {
      'grant_type': 'client_credentials',
      'client_id': 'YOUR_CLIENT_ID',
      'client_secret': 'YOUR_CLIENT_SECRET',
    },
  );

  if (response.statusCode == 200) {
    // Step 2: Extract the access token from the response
    final token = jsonDecode(response.body)['access_token'];
    
    // Step 3: Use the access token to make an authenticated API request
    final apiResponse = await http.get(
      Uri.parse('https://example.com/api'),
      headers: {
        'Authorization': 'Bearer $token',
      },
    );

    if (apiResponse.statusCode == 200) {
      print('Response data: ${apiResponse.body}');
    } else {
      print('Failed to load data');
    }
  } else {
    print('Failed to authenticate');
  }
}
        

Explanation:

  • grant_type, client_id, client_secret: Parameters required for OAuth 2.0 authentication.
  • Bearer: A header to include the access token.

Network Interceptors

Interceptors can be used to log requests and responses, modify headers, or handle errors globally.

// Class to log HTTP requests and responses
class LoggingInterceptor extends http.BaseClient {
  final http.Client _inner;

  LoggingInterceptor(this._inner);

  @override
  Future send(http.BaseRequest request) async {
    // Log the request method and URL
    print('Request: ${request.method} ${request.url}');
    
    // Send the request and wait for the response
    final response = await _inner.send(request);
    
    // Log the response status code
    print('Response: ${response.statusCode}');
    
    return response;
  }
}

void main() {
  // Create a new instance of the LoggingInterceptor
  final client = LoggingInterceptor(http.Client());

  // Make a GET request using the LoggingInterceptor
  client.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1')).then((response) {
    print('Response body: ${response.body}');
  });
}
        

Explanation:

  • LoggingInterceptor: A custom client that logs the request method, URL, and response status code.
  • send: An overridden method to send the request and log the details.

Advanced Network Interceptor

An advanced interceptor can modify requests and responses, add headers, and handle errors globally.

// Class to intercept and modify HTTP requests and responses
class AdvancedInterceptor extends http.BaseClient {
  final http.Client _client;

  AdvancedInterceptor(this._client);

  @override
  Future send(http.BaseRequest request) async {
    // Add custom headers
    request.headers['Custom-Header'] = 'CustomValue';
    
    // Log the request
    print('Sending request: ${request.method} ${request.url}');
    
    // Send the request and get the response
    final response = await _client.send(request);

    // Log the response
    print('Response status: ${response.statusCode}');

    // Modify response if necessary
    if (response.statusCode == 401) {
      // Handle unauthorized error
      print('Unauthorized request');
      // Retry the request or refresh token logic here
    }
    
    return response;
  }
}

void main() {
  // Create a new instance of the AdvancedInterceptor
  final client = AdvancedInterceptor(http.Client());

  // Make a GET request using the AdvancedInterceptor
  client.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1')).then((response) {
    print('Response body: ${response.body}');
  });
}
        

Explanation:

  • AdvancedInterceptor: A custom client that adds headers, logs requests and responses, and handles errors.
  • send: An overridden method to send the request, add headers, log details, and handle errors.

Example: Complete CRUD Operations

// Class to manage API services
class ApiService {
  final String baseUrl;
  Map headers = {};

  ApiService({required this.baseUrl});

  // Method to set the authorization token
  void setToken(String token) {
    headers['Authorization'] = 'Bearer $token';
  }

  // Method to perform a GET request
  Future getData(String endpoint) async {
    final url = Uri.parse('$baseUrl/$endpoint');
    return await http.get(url, headers: headers);
  }

  // Method to perform a POST request
  Future postData(String endpoint, dynamic data) async {
    final url = Uri.parse('$baseUrl/$endpoint');
    return await http.post(
      url,
      headers: {
        ...headers,
        'Content-Type': 'application/json',
      },
      body: jsonEncode(data),
    );
  }

  // Method to perform a PUT request (update)
  Future updateData(String endpoint, dynamic data) async {
    final url = Uri.parse('$baseUrl/$endpoint');
    return await http.put(
      url,
      headers: {
        ...headers,
        'Content-Type': 'application/json',
      },
      body: jsonEncode(data),
    );
  }

  // Method to perform a DELETE request
  Future deleteData(String endpoint) async {
    final url = Uri.parse('$baseUrl/$endpoint');
    return await http.delete(url, headers: headers);
  }
}

void main() {
  // Create a new instance of the ApiService
  final apiService = ApiService(baseUrl: 'https://jsonplaceholder.typicode.com');

  // Example Usage of the ApiService
  apiService.getData('posts/1').then((response) {
    print('GET Response: ${response.body}');
  });

  apiService.postData('posts', {
    'title': 'New Post',
    'body': 'This is the body of the new post',
    'userId': '1',
  }).then((response) {
    print('POST Response: ${response.body}');
  });

  apiService.updateData('posts/1', {
    'title': 'Updated Post',
    'body': 'This is the updated body of the post',
    'userId': '1',
  }).then((response) {
    print('PUT Response: ${response.body}');
  });

  apiService.deleteData('posts/1').then((response) {
    print('DELETE Response: ${response.statusCode}');
  });
}
        

Explanation:

  • ApiService: A class to manage API services, including methods for GET, POST, PUT, and DELETE requests.
  • setToken: A method to set the authorization token.
  • getData, postData, updateData, deleteData: Methods to perform CRUD operations.

Advanced Topics

Network Logging

Using the http package with logging to debug network requests and responses:

// Class to log HTTP requests and responses
class LoggingClient extends http.BaseClient {
  final http.Client _client;

  LoggingClient(this._client);

  @override
  Future send(http.BaseRequest request) async {
    // Log the request method and URL
    print('Request: ${request.method} ${request.url}');
    
    // Send the request and wait for the response
    final response = await _client.send(request);
    
    // Log the response status code
    print('Response: ${response.statusCode}');
    
    return response;
  }
}

void main() {
  // Create a new instance of the LoggingClient
  final client = LoggingClient(http.Client());

  // Make a GET request using the LoggingClient
  client.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1')).then((response) {
    print('Response body: ${response.body}');
  });
}
        

Explanation:

  • LoggingClient: A custom client that logs the request method, URL, and response status code.
  • send: An overridden method to send the request and log the details.

Network Interceptors

Interceptors are middleware that can be used to inspect, modify, or react to requests and responses.

// Class to intercept and modify HTTP requests and responses
class InterceptedClient extends http.BaseClient {
  final http.Client _client;

  InterceptedClient(this._client);

  @override
  Future send(http.BaseRequest request) async {
    // Modify request headers
    request.headers['Custom-Header'] = 'CustomValue';
    
    // Send the request and wait for the response
    final response = await _client.send(request);

    // Log the response status code and reason phrase
    print('Response: ${response.statusCode} ${response.reasonPhrase}');
    
    return response;
  }
}

void main() {
  // Create a new instance of the InterceptedClient
  final client = InterceptedClient(http.Client());

  // Make a GET request using the InterceptedClient
  client.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1')).then((response) {
    print('Response body: ${response.body}');
  });
}
        

Explanation:

  • InterceptedClient: A custom client that modifies the request headers and logs the response details.
  • send: An overridden method to send the request, modify headers, and log the response.

SSL Pinning

SSL pinning is a security technique used to ensure that the app communicates only with a trusted server. Here's how you can implement SSL pinning in Flutter:

import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/io_client.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/services.dart' show rootBundle;

// Function to create an HTTP client with SSL pinning
Future createHttpClient() async {
  // Load the trusted certificate
  final sslCert = await rootBundle.load('assets/certificates/your_certificate.pem');
  SecurityContext context = SecurityContext(withTrustedRoots: false);
  context.setTrustedCertificatesBytes(sslCert.buffer.asInt8List());

  // Create an HTTP client that uses the security context
  HttpClient httpClient = HttpClient(context: context);
  return IOClient(httpClient);
}

// Function to fetch data with SSL pinning
Future fetchDataWithSslPinning() async {
  final client = await createHttpClient();
  final response = await client.get(Uri.parse('https://your-secure-api

.com/data'));

  if (response.statusCode == 200) {
    print('Response data: ${response.body}');
  } else {
    print('Failed to load data');
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('SSL Pinning Example')),
        body: Center(
          child: ElevatedButton(
            onPressed: fetchDataWithSslPinning,
            child: Text('Fetch Data with SSL Pinning'),
          ),
        ),
      ),
    );
  }
}
        

Explanation:

  • createHttpClient: A function that creates an HTTP client with SSL pinning.
  • rootBundle.load: Loads the trusted certificate from the assets folder.
  • SecurityContext: A class that holds the SSL configuration.
  • setTrustedCertificatesBytes: Sets the trusted certificates in the security context.
  • IOClient: A client that uses the security context.

Example: Full Authentication Flow

Sign Up

// Function to sign up a new user
Future signUp(String email, String password) async {
  // Perform a POST request to the sign-up endpoint
  final response = await http.post(
    Uri.parse('https://example.com/signup'),
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode({
      'email': email,
      'password': password,
    }),
  );

  // Check if the request was successful
  if (response.statusCode == 201) {
    print('Sign Up Successful');
  } else {
    print('Failed to sign up');
  }
}
        

Explanation:

  • signUp: A function to sign up a new user by sending a POST request to the sign-up endpoint.
  • headers: Specifies that the content type is JSON.
  • body: Contains the email and password, encoded as a JSON string.

Log In

// Function to log in a user
Future logIn(String email, String password) async {
  // Perform a POST request to the login endpoint
  final response = await http.post(
    Uri.parse('https://example.com/login'),
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode({
      'email': email,
      'password': password,
    }),
  );

  // Check if the request was successful
  if (response.statusCode == 200) {
    // Extract the token from the response
    final token = jsonDecode(response.body)['token'];
    
    // Store the token
    print('Login Successful, token: $token');
  } else {
    print('Failed to log in');
  }
}
        

Explanation:

  • logIn: A function to log in a user by sending a POST request to the login endpoint.
  • headers: Specifies that the content type is JSON.
  • body: Contains the email and password, encoded as a JSON string.
  • token: Extracted from the response and printed.

Token Management

// Class to manage authentication tokens
class TokenManager {
  String? _token;

  // Method to set the token
  void setToken(String token) {
    _token = token;
  }

  // Method to get the token
  String? getToken() {
    return _token;
  }

  // Method to clear the token
  void clearToken() {
    _token = null;
  }
}

final tokenManager = TokenManager();

// Usage example
void main() {
  // Set the token
  tokenManager.setToken('your_token_here');
  
  // Get and print the token
  final token = tokenManager.getToken();
  print('Stored token: $token');
  
  // Clear the token
  tokenManager.clearToken();
}
        

Explanation:

  • TokenManager: A class to manage authentication tokens.
  • setToken: A method to set the token.
  • getToken: A method to retrieve the token.
  • clearToken: A method to clear the token.

Full Tutorial with Working Example

Step-by-Step Guide

  1. Create a new Flutter project by running flutter create http_example in your terminal.
  2. Navigate to the project directory: cd http_example.
  3. Add the http package to your pubspec.yaml file:
dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.3
        
  1. Import the http package in your lib/main.dart file:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('HTTP Example')),
        body: Center(child: FetchDataButton()),
      ),
    );
  }
}

class FetchDataButton extends StatelessWidget {
  // Function to fetch data from an API using a GET request
  Future fetchData() async {
    // Perform a GET request to the given URL
    final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));

    // Check if the request was successful
    if (response.statusCode == 200) {
      // If successful, print the response body
      print('Response data: ${response.body}');
    } else {
      // If not successful, print an error message
      print('Failed to load data');
    }
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: fetchData,
      child: Text('Fetch Data'),
    );
  }
}
        

Explanation:

  • main: The entry point of the Flutter application. It runs the MyApp widget.
  • MyApp: A stateless widget that builds the main application UI. It consists of a Scaffold with an AppBar and a centered FetchDataButton widget.
  • FetchDataButton: A stateless widget that contains a button to fetch data from an API. The fetchData function performs a GET request to the specified URL and prints the response data or an error message.

Adding More Features

To expand this example, let's add the ability to handle POST requests, manage session tokens, and use network interceptors.

Handling POST Requests

Update the FetchDataButton widget to include a method for sending POST requests:

class FetchDataButton extends StatelessWidget {
  // Function to fetch data from an API using a GET request
  Future fetchData() async {
    // Perform a GET request to the given URL
    final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));

    // Check if the request was successful
    if (response.status

Code == 200) {
      // If successful, print the response body
      print('Response data: ${response.body}');
    } else {
      // If not successful, print an error message
      print('Failed to load data');
    }
  }

  // Function to send data to an API using a POST request
  Future sendData() async {
    // Perform a POST request to the given URL
    final response = await http.post(
      Uri.parse('https://jsonplaceholder.typicode.com/posts'),
      headers: {
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode({
        'title': 'Flutter POST Request',
        'body': 'This is a POST request example',
        'userId': '1',
      }),
    );

    // Check if the request was successful
    if (response.statusCode == 201) {
      // If successful, print the response body
      print('Response data: ${response.body}');
    } else {
      // If not successful, print an error message
      print('Failed to send data');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        ElevatedButton(
          onPressed: fetchData,
          child: Text('Fetch Data'),
        ),
        ElevatedButton(
          onPressed: sendData,
          child: Text('Send Data'),
        ),
      ],
    );
  }
}
        

Explanation:

  • sendData: A function to send data to an API using a POST request. It performs a POST request to the specified URL with headers indicating the content type is JSON and a body containing the data to send.
  • Column: A widget that arranges its children in a vertical sequence. It contains two buttons: one for fetching data and another for sending data.
Session Management

To manage session tokens, let's create a SessionManager class and update our example to use it:

class SessionManager {
  Map _headers = {};

  // Method to set the authorization token
  void setToken(String token) {
    _headers['Authorization'] = 'Bearer $token';
  }

  // Method to perform a GET request with the session headers
  Future get(String url) async {
    return await http.get(Uri.parse(url), headers: _headers);
  }

  // Method to perform a POST request with the session headers
  Future post(String url, {Map? headers, dynamic body}) async {
    return await http.post(Uri.parse(url), headers: {..._headers, ...?headers}, body: body);
  }
}

class FetchDataButton extends StatelessWidget {
  final SessionManager sessionManager = SessionManager();

  FetchDataButton() {
    // Set a dummy token for demonstration purposes
    sessionManager.setToken('dummy_token');
  }

  // Function to fetch data using the session manager
  Future fetchData() async {
    final response = await sessionManager.get('https://jsonplaceholder.typicode.com/posts/1');

    if (response.statusCode == 200) {
      print('Response data: ${response.body}');
    } else {
      print('Failed to load data');
    }
  }

  // Function to send data using the session manager
  Future sendData() async {
    final response = await sessionManager.post(
      'https://jsonplaceholder.typicode.com/posts',
      headers: {
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode({
        'title': 'Flutter POST Request',
        'body': 'This is a POST request example',
        'userId': '1',
      }),
    );

    if (response.statusCode == 201) {
      print('Response data: ${response.body}');
    } else {
      print('Failed to send data');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        ElevatedButton(
          onPressed: fetchData,
          child: Text('Fetch Data'),
        ),
        ElevatedButton(
          onPressed: sendData,
          child: Text('Send Data'),
        ),
      ],
    );
  }
}
        

Explanation:

  • SessionManager: A class to manage session tokens and perform HTTP requests with the session headers.
  • setToken: Sets the authorization token in the session headers.
  • get: Performs a GET request with the session headers.
  • post: Performs a POST request with the session headers.
  • FetchDataButton: A stateless widget that uses the SessionManager to perform GET and POST requests.
Using Network Interceptors

To add logging capabilities, let's create a LoggingInterceptor class and integrate it with our example:

class LoggingInterceptor extends http.BaseClient {
  final http.Client _inner;

  LoggingInterceptor(this._inner);

  @override
  Future send(http.BaseRequest request) async {
    // Log the request method and URL
    print('Request: ${request.method} ${request.url}');
    
    // Send the request and wait for the response
    final response = await _inner.send(request);
    
    // Log the response status code
    print('Response: ${response.statusCode}');
    
    return response;
  }
}

class FetchDataButton extends StatelessWidget {
  final http.Client client = LoggingInterceptor(http.Client());

  // Function to fetch data using the logging interceptor
  Future fetchData() async {
    final response = await client.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));

    if (response.statusCode == 200) {
      print('Response data: ${response.body}');
    } else {
      print('Failed to load data');
    }
  }

  // Function to send data using the logging interceptor
  Future sendData() async {
    final response = await client.post(
      Uri.parse('https://jsonplaceholder.typicode.com/posts'),
      headers: {
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode({
        'title': 'Flutter POST Request',
        'body': 'This is a POST request example',
        'userId': '1',
      }),
    );

    if (response.statusCode == 201) {
      print('Response data: ${response.body}');
    } else {
      print('Failed to send data');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        ElevatedButton(
          onPressed: fetchData,
          child: Text('Fetch Data'),
        ),
        ElevatedButton(
          onPressed: sendData,
          child: Text('Send Data'),
        ),
      ],
    );
  }
}
        

Explanation:

  • LoggingInterceptor: A custom client that logs the request method, URL, and response status code.
  • send: An overridden method to send the request and log the details.
  • FetchDataButton: A stateless widget that uses the LoggingInterceptor to perform GET and POST requests.

More Advanced Topics

Handling Multipart Requests

Multipart requests are useful for uploading files along with form data. Here's how you can handle multipart requests:

// Function to upload a file using a multipart request
Future uploadFile(File file) async {
  // Create a multipart request
  var request = http.MultipartRequest('POST', Uri.parse('https://example.com/upload'));

  // Add the file to the request
  request.files.add(await http.MultipartFile.fromPath('file', file.path));

  // Send the request and get the response
  var response = await request.send();

  // Check if the request was successful
  if (response.statusCode == 200) {
    print('File uploaded successfully');
  } else {
    print('Failed to upload file');
  }
}

void main() {
  // Create a sample file (for demonstration purposes)
  File sample

File = File('path/to/sample.txt');

  // Upload the file
  uploadFile(sampleFile);
}
        

Explanation:

  • http.MultipartRequest: Creates a new multipart request.
  • request.files.add: Adds a file to the request.
  • request.send: Sends the multipart request and gets the response.
  • File: Represents a file in Dart. You need to import the dart:io package to use this class.
Retrying Failed Requests

Sometimes, you might want to retry failed requests. Here's how you can implement a simple retry mechanism:

// Function to perform a GET request with retries
Future fetchDataWithRetries({int retries = 3}) async {
  int attempt = 0;
  while (attempt < retries) {
    // Perform a GET request to the given URL
    final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));

    // Check if the request was successful
    if (response.statusCode == 200) {
      // If successful, print the response body
      print('Response data: ${response.body}');
      return;
    } else {
      // If not successful, increment the attempt counter
      attempt++;
      print('Failed to load data. Attempt $attempt/$retries');
    }

    // Wait for 2 seconds before retrying
    await Future.delayed(Duration(seconds: 2));
  }
}

void main() {
  // Fetch data with retries
  fetchDataWithRetries();
}
        

Explanation:

  • fetchDataWithRetries: A function to perform a GET request with retries.
  • retries: The number of retry attempts (default is 3).
  • attempt: The current attempt number.
  • Future.delayed: Waits for the specified duration before continuing.
Handling Timeouts

Handling timeouts is crucial to ensure your application remains responsive. Here's how you can handle timeouts:

// Function to perform a GET request with a timeout
Future fetchDataWithTimeout() async {
  try {
    // Perform a GET request to the given URL with a timeout
    final response = await http
        .get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'))
        .timeout(Duration(seconds: 5));

    // Check if the request was successful
    if (response.statusCode == 200) {
      // If successful, print the response body
      print('Response data: ${response.body}');
    } else {
      // If not successful, print an error message
      print('Failed to load data');
    }
  } on TimeoutException catch (e) {
    // Handle timeout
    print('Request timed out');
  }
}

void main() {
  // Fetch data with a timeout
  fetchDataWithTimeout();
}
        

Explanation:

  • fetchDataWithTimeout: A function to perform a GET request with a timeout.
  • timeout: Specifies the duration to wait before timing out the request.
  • TimeoutException: An exception that occurs when a timeout is reached.
Handling Caching

Caching responses can improve performance and reduce server load. Here's a basic example of handling caching:

import 'package:path_provider/path_provider.dart';
import 'dart:io';

// Class to manage caching
class CacheManager {
  final String cacheKey;
  final Duration cacheDuration;

  CacheManager(this.cacheKey, this.cacheDuration);

  // Method to get the cache file
  Future _getCacheFile() async {
    final directory = await getTemporaryDirectory();
    return File('${directory.path}/$cacheKey.json');
  }

  // Method to get cached data
  Future getCachedData() async {
    final cacheFile = await _getCacheFile();
    if (await cacheFile.exists()) {
      final cacheDate = await cacheFile.lastModified();
      if (DateTime.now().difference(cacheDate) < cacheDuration) {
        return await cacheFile.readAsString();
      } else {
        await cacheFile.delete();
      }
    }
    return null;
  }

  // Method to cache data
  Future cacheData(String data) async {
    final cacheFile = await _getCacheFile();
    await cacheFile.writeAsString(data);
  }
}

// Function to fetch data with caching
Future fetchDataWithCaching() async {
  final cacheManager = CacheManager('posts', Duration(minutes: 10));

  // Try to get cached data
  final cachedData = await cacheManager.getCachedData();
  if (cachedData != null) {
    print('Using cached data: $cachedData');
    return;
  }

  // Perform a GET request to the given URL
  final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));

  // Check if the request was successful
  if (response.statusCode == 200) {
    // If successful, cache the response data and print it
    await cacheManager.cacheData(response.body);
    print('Response data: ${response.body}');
  } else {
    // If not successful, print an error message
    print('Failed to load data');
  }
}

void main() {
  // Fetch data with caching
  fetchDataWithCaching();
}
        

Explanation:

  • CacheManager: A class to manage caching of data.
  • _getCacheFile: A method to get the cache file.
  • getCachedData: A method to get cached data if it exists and is still valid.
  • cacheData: A method to cache data.
  • fetchDataWithCaching: A function to fetch data with caching.
  • getTemporaryDirectory: A function to get the temporary directory for storing the cache file. You need to import the path_provider package to use this function.
Implementing a RESTful API Client

To make your code more modular and reusable, you can implement a RESTful API client. Here's a basic implementation:

// Class to manage API services
class ApiClient {
  final String baseUrl;
  final Map headers;

  ApiClient({required this.baseUrl, required this.headers});

  // Method to perform a GET request
  Future get(String endpoint) async {
    final url = Uri.parse('$baseUrl/$endpoint');
    return await http.get(url, headers: headers);
  }

  // Method to perform a POST request
  Future post(String endpoint, dynamic data) async {
    final url = Uri.parse('$baseUrl/$endpoint');
    return await http.post(
      url,
      headers: {
        ...headers,
        'Content-Type': 'application/json',
      },
      body: jsonEncode(data),
    );
  }

  // Method to perform a PUT request
  Future put(String endpoint, dynamic data) async {
    final url = Uri.parse('$baseUrl/$endpoint');
    return await http.put(
      url,
      headers: {
        ...headers,
        'Content-Type': 'application/json',
      },
      body: jsonEncode(data),
    );
  }

  // Method to perform a DELETE request
  Future delete(String endpoint) async {
    final url = Uri.parse('$baseUrl/$endpoint');
    return await http.delete(url, headers: headers);
  }
}

void main() {
  // Create a new instance of the ApiClient
  final apiClient = ApiClient(baseUrl: 'https://jsonplaceholder.typicode.com', headers: {});

  // Example usage of the ApiClient
  apiClient.get('posts/1').then((response) {
    print('GET Response: ${response.body}');
  });

  apiClient.post('posts', {
    'title': 'New Post',
    'body': 'This is the body of the new post',
    'userId': '1',
  }).then((response) {
    print('POST Response: ${response.body

}');
  });

  apiClient.put('posts/1', {
    'title': 'Updated Post',
    'body': 'This is the updated body of the post',
    'userId': '1',
  }).then((response) {
    print('PUT Response: ${response.body}');
  });

  apiClient.delete('posts/1').then((response) {
    print('DELETE Response: ${response.statusCode}');
  });
}
        

Explanation:

  • ApiClient: A class to manage API services, including methods for GET, POST, PUT, and DELETE requests.
  • get: Performs a GET request to the specified endpoint.
  • post: Performs a POST request to the specified endpoint with the provided data.
  • put: Performs a PUT request to the specified endpoint with the provided data.
  • delete: Performs a DELETE request to the specified endpoint.

Conclusion

The http package in Flutter is an essential tool for making HTTP requests and handling responses. From basic GET and POST requests to advanced features like authentication, session management, and interceptors, the http package provides all the functionality needed for effective network communication. By mastering these concepts and utilizing the provided examples, you can confidently integrate REST APIs and handle network operations in your Flutter applications.

User Comments (0)

Add Comment
We'll never share your email with anyone else.