Using Retrofit for Cleaner API Calls in Flutter

In Flutter development, making network requests is a common and essential task. While Flutter provides built-in HTTP client libraries, using them directly can often lead to verbose and less maintainable code. This is where Retrofit, a type-safe HTTP client generator for Dart, comes into play. By leveraging Retrofit, you can create cleaner, more organized, and easily manageable API calls in your Flutter applications.

What is Retrofit?

Retrofit is a REST client generator for Dart (and therefore, Flutter) based on annotations. It automates the process of making network requests by generating code based on the interfaces you define. This reduces boilerplate code, makes your network layer type-safe, and promotes a more declarative approach to API interactions.

Why Use Retrofit in Flutter?

  • Code Generation: Automatically generates necessary code for making API requests.
  • Type Safety: Ensures that your API calls and data models are type-safe, reducing runtime errors.
  • Reduces Boilerplate: Eliminates verbose HTTP client code, leading to cleaner and more readable code.
  • Annotation-Based: Uses annotations to define API endpoints and request parameters, providing a clear and concise interface.
  • Easy Error Handling: Simplifies error handling and response parsing.

How to Implement Retrofit in Flutter

To implement Retrofit in your Flutter project, follow these steps:

Step 1: Add Dependencies

Add the necessary dependencies to your pubspec.yaml file:

dependencies:
  retrofit: ^4.1.0
  dio: ^5.0.0

dev_dependencies:
  retrofit_generator: ^6.0.0
  build_runner: ^2.0.0
  • retrofit: The Retrofit library itself.
  • dio: HTTP client that Retrofit uses under the hood. Dio is a powerful Http client for Dart, which supports Interceptors, FormData, Request Cancellation, Timeout, etc.
  • retrofit_generator: Generates the API client code based on your interface definitions.
  • build_runner: A tool to run code generators like retrofit_generator.

Step 2: Define Your API Service Interface

Create a Dart interface that defines your API endpoints using annotations:

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

part 'api_service.g.dart';

@RestApi(baseUrl: "https://jsonplaceholder.typicode.com/")
abstract class ApiService {
  factory ApiService(Dio dio, {String baseUrl}) = _ApiService;

  @GET("/posts")
  Future<List<Post>> getPosts();

  @GET("/posts/{id}")
  Future<Post> getPost(@Path("id") int id);

  @POST("/posts")
  Future<Post> createPost(@Body() Post post);

  @PUT("/posts/{id}")
  Future<Post> updatePost(@Path("id") int id, @Body() Post post);

  @DELETE("/posts/{id}")
  Future<void> deletePost(@Path("id") int id);
}

// Data model
class Post {
  int? userId;
  int? id;
  String? title;
  String? body;

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

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

  Map<String, dynamic> toJson() {
    return {
      'userId': userId,
      'id': id,
      'title': title,
      'body': body,
    };
  }
}

In this interface:

  • @RestApi(baseUrl: "https://jsonplaceholder.typicode.com/") sets the base URL for all API endpoints.
  • @GET("/posts") defines an endpoint to retrieve all posts.
  • @GET("/posts/{id}") defines an endpoint to retrieve a specific post by ID, using the @Path annotation.
  • @POST("/posts") defines an endpoint to create a new post, sending data in the request body using the @Body annotation.
  • @PUT("/posts/{id}") defines an endpoint to update an existing post, sending data in the request body using the @Body annotation.
  • @DELETE("/posts/{id}") defines an endpoint to delete a post by ID.

Step 3: Generate the Retrofit Client

Run the build runner to generate the Retrofit client code:

flutter pub run build_runner build

This command generates the api_service.g.dart file, which contains the implementation of the ApiService interface.

Step 4: Use the API Service in Your Flutter App

Now, you can use the generated ApiService in your Flutter app:

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Retrofit Example',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late ApiService _apiService;
  List<Post> _posts = [];

  @override
  void initState() {
    super.initState();
    final dio = Dio();
    _apiService = ApiService(dio);
    _fetchPosts();
  }

  Future<void> _fetchPosts() async {
    try {
      final posts = await _apiService.getPosts();
      setState(() {
        _posts = posts;
      });
    } catch (e) {
      print("Error fetching posts: $e");
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Retrofit Example'),
      ),
      body: ListView.builder(
        itemCount: _posts.length,
        itemBuilder: (context, index) {
          final post = _posts[index];
          return ListTile(
            title: Text(post.title ?? 'No Title'),
            subtitle: Text(post.body ?? 'No Body'),
          );
        },
      ),
    );
  }
}

Advanced Usage

Handling Different Response Types

You can use Retrofit to handle different types of responses, such as JSON objects, lists, and raw data. The key is to define the appropriate return type in your API service interface.

Using Interceptors

Interceptors allow you to intercept and modify HTTP requests and responses. They can be useful for adding headers, logging requests, and handling authentication. Retrofit uses Dio under the hood, so you use Dio’s interceptors.

import 'package:dio/dio.dart';

class AuthInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    // Add authentication header
    options.headers['Authorization'] = 'Bearer YOUR_AUTH_TOKEN';
    super.onRequest(options, handler);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    print('ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}');
    super.onError(err, handler);
  }
}


// In your main function or initialization
final dio = Dio();
dio.interceptors.add(AuthInterceptor());
final apiService = ApiService(dio);

Error Handling

Handle exceptions thrown by the API service to gracefully manage errors.


try {
  final post = await _apiService.getPost(1);
  print('Post title: ${post.title}');
} catch (e) {
  if (e is DioException) {
    // Handle Dio-specific errors (e.g., network issues, server errors)
    print('DioError: ${e.message}');
    print('Status code: ${e.response?.statusCode}');
  } else {
    // Handle generic errors
    print('Error: $e');
  }
}

Conclusion

Using Retrofit in Flutter offers a streamlined and efficient approach to making API calls. It reduces boilerplate code, enhances type safety, and promotes a more organized and maintainable network layer. By adopting Retrofit, you can focus on building features rather than wrestling with low-level HTTP client details. Embrace Retrofit to create cleaner and more robust Flutter applications that seamlessly interact with your backend services.