swadana

Here is the complete translation of the tutorial into English, preserving the structured code blocks, technical accuracy, and formatting.

Introduction: Why REST API Integration Matters in Your Flutter App?

Welcome to AnakInformatika! If you are a Flutter developer looking to build dynamic, interactive, and always up-to-date applications, mastering this Tutorial on Integrating REST APIs with HTTP / Dio in Flutter is an essential skill. Modern applications almost always rely on data coming from external servers, which is where REST APIs play a crucial role.

REST (Representational State Transfer) API allows our application to communicate with a server to retrieve data (GET), send new data (POST), update data (PUT/PATCH), or delete data (DELETE). In the Flutter ecosystem, there are two popular libraries that serve as the primary choices for handling these HTTP requests: the http package and Dio. Both have their own advantages and disadvantages, and this tutorial will cover both in-depth, providing you with a comprehensive understanding and production-ready code examples.

Let's dive into how to make your Flutter application come "alive" by interacting with the outside world!

Prerequisites

Before starting this API integration journey, make sure you meet the following requirements:

  • Flutter SDK: Installed and properly configured on your system.

  • IDE: Visual Studio Code or Android Studio with the Flutter plugin installed.

  • Basic Knowledge of Dart & Flutter: Familiarity with basic concepts such as Widgets, State, StatelessWidget/StatefulWidget, and Futures.

  • Internet Connection: Required to download packages and access the API.

Flutter Project Setup

First, let's create a new Flutter project and add the necessary dependencies. Open your terminal or command prompt and run the following commands:

Bash
flutter create rest_api_integration_demo
cd rest_api_integration_demo

Next, open your pubspec.yaml file and add the http and dio packages under the dependencies: section. We will use JSONPlaceholder as a free, public REST API for this demo.

YAML
dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.1 # Latest version at the time of writing
  dio: ^5.4.3+1 # Latest version at the time of writing
  # Add other packages if needed

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0

Save the pubspec.yaml file and run flutter pub get to download the packages.

Creating the Data Model (Serialization)

To manage the data received from the API (which is typically in JSON format), it is highly recommended to create a data model (Dart class). This helps in deserializing JSON into Dart objects and vice versa (serialization). We will use the "Posts" data from JSONPlaceholder.

Create a models folder inside the lib directory, then create a file named post.dart:

Dart
// lib/models/post.dart
import 'dart:convert';

class Post {
  final int? userId;
  final int? id;
  final String title;
  final String body;

  Post({
    this.userId,
    this.id,
    required this.title,
    required this.body,
  });

  // Factory constructor to create a Post instance from JSON (deserialization)
  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      userId: json['userId'] as int?,
      id: json['id'] as int?,
      title: json['title'] as String,
      body: json['body'] as String,
    );
  }

  // Method to convert a Post instance into JSON (serialization)
  Map<String, dynamic> toJson() {
    return {
      'userId': userId,
      'id': id,
      'title': title,
      'body': body,
    };
  }
}

Model Code Explanation:

  • Post.fromJson(Map<String, dynamic> json): This is a factory constructor used to convert JSON data (in the form of a Map<String, dynamic>) into a Post object. This is essential when you receive a response from the API.

  • toJson(): This method does the opposite—it converts a Post object into a Map<String, dynamic>, which can then be encoded into a JSON string to be sent to the API (e.g., for POST or PUT requests).

Integrating REST API with the HTTP Package

The http package is Dart's built-in, lightweight, and easy-to-use HTTP client. It is perfect for simple projects or when you do not require advanced features like interceptors or caching available in Dio.

Creating the API Service with HTTP

Create a services folder inside lib, then create a file named post_service_http.dart:

Dart
// lib/services/post_service_http.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:rest_api_integration_demo/models/post.dart';

class PostServiceHttp {
  final String _baseUrl = 'https://jsonplaceholder.typicode.com';

  // --- GET Request ---
  Future<List<Post>> fetchPosts() async {
    final response = await http.get(Uri.parse('$_baseUrl/posts'));

    if (response.statusCode == 200) {
      // If the server returns an OK response (status code 200), parse the JSON.
      List<dynamic> data = jsonDecode(response.body);
      return data.map((json) => Post.fromJson(json)).toList();
    } else {
      // If the server does not return an OK response, throw an exception.
      throw Exception('Failed to load posts: ${response.statusCode}');
    }
  }

  // --- POST Request ---
  Future<Post> createPost(Post post) async {
    final response = await http.post(
      Uri.parse('$_baseUrl/posts'),
      headers: <String, String>{
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode(post.toJson()), // Converts Post object to JSON string
    );

    if (response.statusCode == 201) { // 201 Created
      return Post.fromJson(jsonDecode(response.body));
    } else {
      throw Exception('Failed to create post: ${response.statusCode}');
    }
  }

  // --- PUT/PATCH Request (for updating) ---
  Future<Post> updatePost(int id, Post post) async {
    final response = await http.put(
      Uri.parse('$_baseUrl/posts/$id'),
      headers: <String, String>{
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode(post.toJson()),
    );

    if (response.statusCode == 200) {
      return Post.fromJson(jsonDecode(response.body));
    } else {
      throw Exception('Failed to update post: ${response.statusCode}');
    }
  }

  // --- DELETE Request ---
  Future<void> deletePost(int id) async {
    final response = await http.delete(
      Uri.parse('$_baseUrl/posts/$id'),
      headers: <String, String>{
        'Content-Type': 'application/json; charset=UTF-8',
      },
    );

    if (response.statusCode == 200) {
      print('Post with ID $id deleted successfully.');
    } else {
      throw Exception('Failed to delete post: ${response.statusCode}');
    }
  }
}

HTTP Service Code Explanation:

  • _baseUrl: Defines the base URL of the API for easier management.

  • http.get(Uri.parse('$_baseUrl/posts')): Sends a GET request to the /posts endpoint. Uri.parse() converts the URL string into a Uri object required by HTTP methods.

  • jsonDecode(response.body): Converts the JSON string received from the server into a Dart object (usually a List<dynamic> or Map<String, dynamic>).

  • data.map((json) => Post.fromJson(json)).toList(): Iterates through each JSON item in the list and converts it into a Post object using the Post.fromJson() factory constructor.

  • response.statusCode == 200: Checks if the request was successful. A status code of 200 means OK, while 201 means Created (used for POST).

  • http.post(...), http.put(...), http.delete(...): Similar to GET, but utilizing different HTTP methods.

  • headers: {'Content-Type': 'application/json; charset=UTF-8'}: Crucial for telling the server that we are sending data in JSON format.

  • body: jsonEncode(post.toJson()): Converts the Post object into a JSON string before sending it to the server.

Using the HTTP Service in the UI

Now, let's use this service inside our application's UI. Modify the main.dart file:

Dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:rest_api_integration_demo/models/post.dart';
import 'package:rest_api_integration_demo/services/post_service_http.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter REST API HTTP Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const PostListPageHttp(),
    );
  }
}

class PostListPageHttp extends StatefulWidget {
  const PostListPageHttp({super.key});

  @override
  State<PostListPageHttp> createState() => _PostListPageHttpState();
}

class _PostListPageHttpState extends State<PostListPageHttp> {
  late Future<List<Post>> _futurePosts;
  final PostServiceHttp _postService = PostServiceHttp();

  @override
  void initState() {
    super.initState();
    _futurePosts = _postService.fetchPosts(); // Call API during initState
  }

  // Example function to create a new post
  Future<void> _createPost() async {
    final newPost = Post(title: 'New Title From HTTP', body: 'New post content from Flutter app using HTTP!');
    try {
      final createdPost = await _postService.createPost(newPost);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Post created successfully: ${createdPost.title}')),
      );
      setState(() {
        _futurePosts = _postService.fetchPosts(); // Refresh list
      });
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to create post: $e')),
      );
    }
  }

  // Example function to delete a post
  Future<void> _deletePost(int id) async {
    try {
      await _postService.deletePost(id);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Post ID $id deleted successfully.')),
      );
      setState(() {
        _futurePosts = _postService.fetchPosts(); // Refresh list
      });
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to delete post: $e')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Posts (HTTP Package)'),
      ),
      body: FutureBuilder<List<Post>>(
        future: _futurePosts,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          } else if (snapshot.hasError) {
            return Center(child: Text('Error: ${snapshot.error}'));
          } else if (snapshot.hasData) {
            return ListView.builder(
              itemCount: snapshot.data!.length,
              itemBuilder: (context, index) {
                final post = snapshot.data![index];
                return Card(
                  margin: const EdgeInsets.all(8.0),
                  child: ListTile(
                    title: Text(post.title),
                    subtitle: Text(post.body),
                    trailing: IconButton(
                      icon: const Icon(Icons.delete, color: Colors.red),
                      onPressed: () => _deletePost(post.id!), // Ensure ID is not null
                    ),
                    onTap: () {
                      // Example: Navigate to detail or update post
                      print('Post ${post.id} selected');
                    },
                  ),
                );
              },
            );
          } else {
            return const Center(child: Text('No post data available.'));
          }
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _createPost,
        child: const Icon(Icons.add),
      ),
    );
  }
}

HTTP UI Code Explanation:

  • FutureBuilder<List<Post>>: A highly useful widget for handling asynchronous operations. It builds the UI based on the latest snapshot of the provided Future (_futurePosts).

  • ConnectionState.waiting: Displays a loading indicator while data is being fetched.

  • snapshot.hasError: Displays an error message if something goes wrong.

  • snapshot.hasData: Displays the data if successfully fetched, using a ListView.builder in this case.

  • initState(): The ideal place to trigger the API call for the first time when the widget is initialized.

  • _createPost() and _deletePost(): Examples of triggering POST and DELETE requests from the UI. After a successful operation, setState is called to refresh the data by refetching _postService.fetchPosts().

Integrating REST API with the Dio Package

Dio is a more advanced and feature-rich HTTP client. It offers interceptors, form data, request cancellation, and much more. It is an excellent choice for larger and more complex applications.

Creating the API Service with Dio

Create a file named post_service_dio.dart inside the lib/services folder:

Dart
// lib/services/post_service_dio.dart
import 'package:dio/dio.dart';
import 'package:rest_api_integration_demo/models/post.dart';

class PostServiceDio {
  final Dio _dio = Dio(BaseOptions(
    baseUrl: 'https://jsonplaceholder.typicode.com',
    connectTimeout: const Duration(seconds: 5), // 5 seconds
    receiveTimeout: const Duration(seconds: 3), // 3 seconds
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
    },
  ));

  // --- GET Request ---
  Future<List<Post>> fetchPosts() async {
    try {
      final response = await _dio.get('/posts');
      if (response.statusCode == 200) {
        List<dynamic> data = response.data;
        return data.map((json) => Post.fromJson(json)).toList();
      } else {
        throw Exception('Failed to load posts: ${response.statusCode}');
      }
    } on DioException catch (e) { // Handle DioException specifically
      throw Exception('Failed to load posts: ${e.message}');
    } catch (e) {
      throw Exception('An unexpected error occurred: $e');
    }
  }

  // --- POST Request ---
  Future<Post> createPost(Post post) async {
    try {
      final response = await _dio.post(
        '/posts',
        data: post.toJson(), // Dio automatically handles JSON encoding
      );

      if (response.statusCode == 201) {
        return Post.fromJson(response.data);
      } else {
        throw Exception('Failed to create post: ${response.statusCode}');
      }
    } on DioException catch (e) {
      throw Exception('Failed to create post: ${e.message}');
    }
  }

  // --- PUT/PATCH Request (for updating) ---
  Future<Post> updatePost(int id, Post post) async {
    try {
      final response = await _dio.put(
        '/posts/$id',
        data: post.toJson(),
      );

      if (response.statusCode == 200) {
        return Post.fromJson(response.data);
      } else {
        throw Exception('Failed to update post: ${response.statusCode}');
      }
    } on DioException catch (e) {
      throw Exception('Failed to update post: ${e.message}');
    }
  }

  // --- DELETE Request ---
  Future<void> deletePost(int id) async {
    try {
      final response = await _dio.delete(
        '/posts/$id',
      );

      if (response.statusCode == 200) {
        print('Post with ID $id deleted successfully.');
      } else {
        throw Exception('Failed to delete post: ${response.statusCode}');
      }
    } on DioException catch (e) {
      throw Exception('Failed to delete post: ${e.message}');
    }
  }

  // Example of using Interceptors (Optional)
  void addInterceptors() {
    _dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (options, handler) {
          // Do something before the request is sent
          print('REQUEST[${options.method}] => PATH: ${options.path}');
          return handler.next(options); // Continue request
        },
        onResponse: (response, handler) {
          // Do something with response data
          print('RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}');
          return handler.next(response); // Continue response
        },
        onError: (DioException e, handler) {
          // Do something with DioError
          print('ERROR[${e.response?.statusCode}] => PATH: ${e.requestOptions.path}');
          return handler.next(e); // Continue error
        },
      ),
    );
  }
}

Dio Service Code Explanation:

  • final Dio _dio = Dio(...): Instantiates Dio. Here, we can globally configure BaseOptions such as baseUrl, connectTimeout, receiveTimeout, and headers for all requests.

  • _dio.get('/posts'): Since the baseUrl is already configured, we only need to provide the relative path.

  • response.data: Dio automatically decodes JSON responses into Dart objects (Map or List), so calling jsonDecode() manually is completely unnecessary.

  • data: post.toJson(): For POST/PUT requests, Dio automatically encodes the provided Dart map into a JSON string, eliminating the need for manual jsonEncode().

  • on DioException catch (e): Dio provides a specialized error type called DioException, which gives detailed information about the issue, such as error types (connection, timeout, etc.) and server responses.

  • addInterceptors(): An example of adding interceptors. Interceptors allow you to intercept and modify requests or responses before they are processed. This is highly beneficial for logging, handling authentication tokens, or global error handling.

Using the Dio Service in the UI

Let's create a new file for the Dio demo so it does not get mixed up with HTTP. Create a file named post_list_page_dio.dart inside the lib/ folder:

Dart
// lib/post_list_page_dio.dart
import 'package:flutter/material.dart';
import 'package:rest_api_integration_demo/models/post.dart';
import 'package:rest_api_integration_demo/services/post_service_dio.dart';

class PostListPageDio extends StatefulWidget {
  const PostListPageDio({super.key});

  @override
  State<PostListPageDio> createState() => _PostListPageDioState();
}

class _PostListPageDioState extends State<PostListPageDio> {
  late Future<List<Post>> _futurePosts;
  final PostServiceDio _postService = PostServiceDio();

  @override
  void initState() {
    super.initState();
    _postService.addInterceptors(); // Enable interceptors (optional)
    _futurePosts = _postService.fetchPosts();
  }

  Future<void> _createPost() async {
    final newPost = Post(title: 'New Title From Dio', body: 'New post content from Flutter app using Dio!');
    try {
      final createdPost = await _postService.createPost(newPost);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Post created successfully: ${createdPost.title}')),
      );
      setState(() {
        _futurePosts = _postService.fetchPosts();
      });
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to create post: $e')),
      );
    }
  }

  Future<void> _deletePost(int id) async {
    try {
      await _postService.deletePost(id);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Post ID $id deleted successfully.')),
      );
      setState(() {
        _futurePosts = _postService.fetchPosts();
      });
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to delete post: $e')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Posts (Dio Package)'),
      ),
      body: FutureBuilder<List<Post>>(
        future: _futurePosts,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          } else if (snapshot.hasError) {
            return Center(child: Text('Error: ${snapshot.error}'));
          } else if (snapshot.hasData) {
            return ListView.builder(
              itemCount: snapshot.data!.length,
              itemBuilder: (context, index) {
                final post = snapshot.data![index];
                return Card(
                  margin: const EdgeInsets.all(8.0),
                  child: ListTile(
                    title: Text(post.title),
                    subtitle: Text(post.body),
                    trailing: IconButton(
                      icon: const Icon(Icons.delete, color: Colors.red),
                      onPressed: () => _deletePost(post.id!),
                    ),
                    onTap: () {
                      print('Post ${post.id} selected');
                    },
                  ),
                );
              },
            );
          } else {
            return const Center(child: Text('No post data available.'));
          }
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _createPost,
        child: const Icon(Icons.add),
      ),
    );
  }
}

To display the Dio page, you can swap the home property in main.dart:

Dart
// lib/main.dart (Inside MyApp component)
// ...
      home: const PostListPageDio(), // Change to this to see the Dio demo
// ...

The UI structure for Dio is almost identical to HTTP because both return a Future<List<Post>>. The primary difference lies inside the implementation details at the service layer.

Practical Tips and Best Practices

  • Separation of Concerns:

    • Model: Exclusively for defining data structures and handling JSON serialization/deserialization.

    • Service/Repository: Manages API call logic, error handling, and converting responses into Dart objects. This keeps your UI clean and detached from raw API logic.

    • UI (Widgets): Focuses solely on data presentation and user interaction by calling methods from the service.

  • Robust Error Handling: Always wrap your API calls in try-catch blocks. Check the statusCode from HTTP responses. When using Dio, handle DioException to catch more specific errors and provide clean feedback (such as a SnackBar or dialog) to the user.

  • State Management: Use FutureBuilder to display asynchronous data gracefully. For complex applications, consider full state management solutions like Provider, Riverpod, BLoC, or GetX to handle loading, error, and data states efficiently.

  • Timeouts: Always configure timeouts for your API requests (connectTimeout, receiveTimeout). This prevents your app from being stuck infinitely waiting for a server response. Dio simplifies this through BaseOptions.

  • Interceptors (Dio Specific): Take advantage of interceptors for:

    • Logging: Outputting every request and response for easy debugging.

    • Authentication: Automatically attaching authentication tokens (e.g., Bearer Tokens) to every outgoing header.

    • Global Error Handling: Handling specific types of server errors centrally in one place.

  • Base URL Management: Always isolate your base URL from specific endpoints. This makes switching environments (development, staging, production) incredibly easy.

  • Security: Never hardcode sensitive API keys directly into your source code. Use environment variables or configuration management solutions. Always use HTTPS for API communication.

  • Testability: By extracting your API logic into a separate service, you can write isolated unit tests for your services much more easily.

Conclusion

Congratulations! You have successfully completed this Tutorial on Integrating REST APIs with HTTP / Dio in Flutter. You now possess a solid understanding of how to fetch, create, update, and delete data using the two most popular HTTP libraries in Flutter: http and Dio.

Both http and Dio are incredibly powerful tools, and choosing between them usually depends on the specific scale of your project. For simple applications, http gets the job done efficiently. For larger applications that demand advanced capabilities like interceptors, global timeouts, and richer error metrics, Dio stands out as the superior choice.

Keep practicing, experiment with different APIs, and feel free to revisit this tutorial whenever you need a reference guide. Integrating REST APIs is one of the most fundamental skills that will take your Flutter development to the next level. See you in the next AnakInformatika tutorial!