Flutter Dio API Call: GET, POST and Error Handling

Flutter Dio API Call
Flutter Dio API Call

Flutter Dio API Call – Making API requests is a common requirement in Flutter applications. Whether you are building a news app, an e-commerce application, or a user management system, you need a reliable way to communicate with a backend server.

In this tutorial, you will learn how to make a Flutter Dio API call using the Dio package. We will cover GET and POST requests, JSON parsing, loading indicators, error handling, and a complete example that fetches data from a REST API.

What Is Dio in Flutter?

Dio is a popular HTTP client for Dart and Flutter. It makes it easier to send HTTP requests, handle responses, configure request headers, set timeouts, upload files, and manage network errors.

Compared with using the basic http package, Dio provides additional features such as interceptors, request cancellation, and centralized configuration.

You can explore the package and its API documentation here:

Step 1: Add the Dio Package

Open your Flutter project in a terminal and run:

flutter pub add dio

Flutter will add Dio to your pubspec.yaml dependencies.

If you prefer to add it manually, use the version suggested by pub.dev rather than copying an outdated version number.

Step 2: Create a Simple API Model

For this tutorial, we will use the JSONPlaceholder demo API to fetch posts.

Create a file named post_model.dart:

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

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

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

The Post.fromJson() factory constructor converts the JSON response into a Dart object. Using a model makes the code easier to read and maintain.

Step 3: Make a GET API Call Using Dio in Flutter

A GET request is used to retrieve data from a server.

Create a file named api_service.dart:

import 'package:dio/dio.dart';
import 'post_model.dart';

class ApiService {
  final Dio _dio = Dio(
    BaseOptions(
      baseUrl: 'https://jsonplaceholder.typicode.com',
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 10),
      headers: {
        'Accept': 'application/json',
      },
    ),
  );

  Future<List<Post>> fetchPosts() async {
    try {
      final response = await _dio.get('/posts');

      final data = response.data as List<dynamic>;

      return data
          .map(
            (item) => Post.fromJson(
              Map<String, dynamic>.from(item as Map),
            ),
          )
          .toList();
    } on DioException catch (error) {
      throw Exception(
        error.message ?? 'Unable to fetch posts.',
      );
    }
  }
}

How does this code work?

  • baseUrl defines the common API address.
  • connectTimeout limits how long Dio waits to establish a connection.
  • receiveTimeout limits the wait for receiving response data.
  • _dio.get('/posts') sends a GET request.
  • response.data contains the decoded response body.
  • Post.fromJson() converts each JSON object into a Post.

Dio automatically decodes JSON responses when the response format is JSON, so you usually do not need to call jsonDecode() manually.

Step 4: Display API Data in Flutter

Now create a screen that calls the API and displays the posts with a loading indicator and error message.

Create posts_screen.dart:

import 'package:flutter/material.dart';
import 'api_service.dart';
import 'post_model.dart';

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

  @override
  State<PostsScreen> createState() => _PostsScreenState();
}

class _PostsScreenState extends State<PostsScreen> {
  final ApiService _apiService = ApiService();

  List<Post> _posts = [];
  bool _isLoading = true;
  String? _errorMessage;

  @override
  void initState() {
    super.initState();
    _loadPosts();
  }

  Future<void> _loadPosts() async {
    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });

    try {
      final posts = await _apiService.fetchPosts();

      if (!mounted) return;

      setState(() {
        _posts = posts;
        _isLoading = false;
      });
    } catch (error) {
      if (!mounted) return;

      setState(() {
        _errorMessage = error.toString();
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Dio API Call'),
        actions: [
          IconButton(
            onPressed: _loadPosts,
            icon: const Icon(Icons.refresh),
            tooltip: 'Refresh posts',
          ),
        ],
      ),
      body: _isLoading
          ? const Center(
              child: CircularProgressIndicator(),
            )
          : _errorMessage != null
              ? Center(
                  child: Padding(
                    padding: const EdgeInsets.all(16),
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Text(
                          _errorMessage!,
                          textAlign: TextAlign.center,
                        ),
                        const SizedBox(height: 12),
                        ElevatedButton(
                          onPressed: _loadPosts,
                          child: const Text('Try Again'),
                        ),
                      ],
                    ),
                  ),
                )
              : ListView.builder(
                  itemCount: _posts.length,
                  itemBuilder: (context, index) {
                    final post = _posts[index];

                    return ListTile(
                      leading: CircleAvatar(
                        child: Text('${post.id}'),
                      ),
                      title: Text(post.title),
                      subtitle: Text(post.body),
                    );
                  },
                ),
    );
  }
}

Finally, update main.dart:

import 'package:flutter/material.dart';
import 'posts_screen.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Dio API Example',
      theme: ThemeData(
        colorSchemeSeed: Colors.blue,
        useMaterial3: true,
      ),
      home: const PostsScreen(),
    );
  }
}

Run the application:

flutter run

If the demo API is reachable, you should see a list of posts. The refresh button sends another request, and the screen shows a loading indicator while waiting for the response.

Step 5: Make a POST API Call Using Dio

A POST request is commonly used to submit data to a backend, such as creating a user or adding a new post.

Add this method to the ApiService class:

Future<Map<String, dynamic>> createPost({
  required String title,
  required String body,
}) async {
  try {
    final response = await _dio.post(
      '/posts',
      data: {
        'title': title,
        'body': body,
        'userId': 1,
      },
    );

    return Map<String, dynamic>.from(
      response.data as Map,
    );
  } on DioException catch (error) {
    throw Exception(
      error.message ?? 'Unable to create post.',
    );
  }
}

You can call the method from a button or form submission handler:

final result = await apiService.createPost(
  title: 'My Flutter Post',
  body: 'This post was created using Dio.',
);

debugPrint('Created post: $result');

JSONPlaceholder simulates creating a post and returns a sample response. It does not permanently save the new post to a real database.

For a production API, use the URL, authentication method, and request fields specified by your backend.

Step 6: Handle Dio API Errors Properly

Network requests can fail for several reasons, including connection problems, timeouts, unsuccessful HTTP status codes, or invalid server responses.

Dio represents request failures with DioException.

Here is a reusable function for displaying a more helpful error message:

String getDioErrorMessage(DioException error) {
  switch (error.type) {
    case DioExceptionType.connectionTimeout:
      return 'Connection timed out. Please try again.';

    case DioExceptionType.sendTimeout:
      return 'Sending the request took too long.';

    case DioExceptionType.receiveTimeout:
      return 'The server took too long to respond.';

    case DioExceptionType.connectionError:
      return 'Unable to connect. Check your internet connection.';

    case DioExceptionType.badResponse:
      final statusCode = error.response?.statusCode;
      return 'Server error: HTTP ${statusCode ?? 'unknown'}.';

    case DioExceptionType.cancel:
      return 'The request was cancelled.';

    case DioExceptionType.badCertificate:
      return 'The server certificate could not be verified.';

    case DioExceptionType.unknown:
      return 'An unexpected network error occurred.';
  }
}

You can use it inside a catch block:

on DioException catch (error) {
  final message = getDioErrorMessage(error);
  debugPrint(message);
}

For a real application, show user-friendly messages in the interface and send diagnostic details to your logging system when appropriate. Avoid exposing internal server responses or sensitive information to users.

Step 7: Use Dio Interceptors

Interceptors allow you to inspect or modify requests and responses centrally. They are useful for logging, adding authentication headers, and handling common response behavior.

For example, add a logging interceptor when configuring Dio during development:

final Dio dio = Dio(
  BaseOptions(
    baseUrl: 'https://jsonplaceholder.typicode.com',
  ),
)..interceptors.add(
    LogInterceptor(
      requestBody: true,
      responseBody: true,
    ),
  );

Use logging carefully in production. Request and response bodies can contain passwords, access tokens, personal information, or other sensitive data. Avoid logging those fields.

Adding an Authorization Header

For a protected API, you may need a bearer token:

final response = await dio.get(
  '/profile',
  options: Options(
    headers: {
      'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
    },
  ),
);

Replace the placeholder with a token obtained through your actual authentication flow. Do not hardcode private credentials or long-lived secrets into a distributed mobile application.

Step 8: Common Mistakes When Using Dio

1. Forgetting to await the API call

Incorrect:

final posts = apiService.fetchPosts();

This returns a Future<List<Post>>, not the list itself.

Correct:

final posts = await apiService.fetchPosts();

2. Calling setState after a widget is disposed

A request may finish after the user leaves the screen. Check mounted before calling setState() after an asynchronous operation.

3. Ignoring unsuccessful HTTP responses

Handle DioException and show a useful error state rather than assuming every request succeeds.

4. Parsing JSON with the wrong data type

An API might return a list, an object, or a nested response. Inspect the actual response structure before writing your model conversion code.

5. Creating a new Dio instance for every request

For larger applications, consider configuring one reusable Dio instance with a shared base URL, timeouts, headers, and interceptors. This makes network behavior easier to maintain.

Frequently Asked Questions

1. How do I make a Dio API call in Flutter?

Add the Dio dependency, create a Dio instance, and call methods such as get() or post(). Use async and await to handle the response.

2. What is the difference between Dio and the http package?

Both packages can send HTTP requests. Dio provides built-in features such as interceptors, request cancellation, configurable timeouts, and convenient request configuration. The best choice depends on your application’s requirements.

3. How do I parse JSON in Flutter using Dio?

Dio normally decodes JSON responses automatically when the response content type is JSON. Convert the resulting map or list into your Dart model using a factory constructor such as Post.fromJson().

4. How do I handle API errors in Dio?

Catch DioException, inspect its type and any available response status code, and display a suitable message. Handle other exceptions separately if your parsing or application logic can fail too.

5. Can I use Dio with Flutter Web?

Yes. Dio supports Flutter Web, but browser security rules such as CORS still apply. The backend server must allow requests from the relevant web origin.

6. Is Dio suitable for large Flutter applications?

Yes. You can use a shared Dio instance, model classes, repositories, interceptors, and a state management solution to organize API communication as your application grows.

Conclusion

Dio is a flexible way to integrate REST APIs into Flutter applications. In this tutorial, you learned how to make a Flutter Dio API call, retrieve data with GET, submit data with POST, parse JSON, display loading and error states, and configure interceptors.

Start with the simple service shown above. As your project grows, separate API services from UI code and introduce repositories or state management where they provide a clear benefit.

For further reading, explore the official Flutter networking guide and the Dio package documentation.

Thank you for visiting us, keep looking for more usefull Flutter Tutorials.

Leave a Comment