Setting Up API Clients with Retrofit & Freezed

Overview

This guide will walk you through setting up type-safe API clients in your Flutter application using Retrofit and Freezed. This combination provides a robust way to handle API interactions with full type safety and immutable data models.

Prerequisites

Add these dependencies to your pubspec.yaml:

dependencies:
  retrofit: ^4.0.1
  json_annotation: ^4.8.1
  freezed_annotation: ^2.4.1
  dio: ^5.3.2

dev_dependencies:
  retrofit_generator: ^7.0.8
  build_runner: ^2.4.6
  json_serializable: ^6.7.1
  freezed: ^2.4.2

Project Structure

Following our template structure, create these directories:

lib/
├── data/
│   ├── api/
│   │   ├── clients/
│   │   │   └── api_client.dart
│   │   └── interceptors/
│   │       └── auth_interceptor.dart
│   └── models/
│       └── user/
│           ├── user.dart
│           ├── user.freezed.dart
│           └── user.g.dart
├── di/
│   └── injection.dart

Creating Data Models with Freezed

Let’s create a sample user model:

// lib/data/models/user/user.dart

import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.g.dart';


class User with _$User {
  const factory User({
    required int id,
    required String name,
    required String email,
    String? avatarUrl,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}

Setting Up the API Client

Create your API client using Retrofit:

// lib/data/api/clients/api_client.dart

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

part 'api_client.g.dart';

(baseUrl: "https://api.yourservice.com")
abstract class ApiClient {
  factory ApiClient(Dio dio, {String baseUrl}) = _ApiClient;

  ("/users/{id}")
  Future<User> getUser(("id") int id);

  ("/users")
  Future<List<User>> getUsers();

  ("/users")
  Future<User> createUser(() User user);
}

Implementing Auth Interceptor

// lib/data/api/interceptors/auth_interceptor.dart

import 'package:dio/dio.dart';

class AuthInterceptor extends Interceptor {
  final String? authToken;

  AuthInterceptor({this.authToken});

  
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    if (authToken != null) {
      options.headers['Authorization'] = 'Bearer $authToken';
    }
    handler.next(options);
  }
}

Dependency Injection Setup

Update your dependency injection configuration:

// lib/di/injection.dart

import 'package:get_it/get_it.dart';
import 'package:dio/dio.dart';
import '../data/api/clients/api_client.dart';
import '../data/api/interceptors/auth_interceptor.dart';

final getIt = GetIt.instance;

void setupApiDependencies() {
  // Register Dio
  getIt.registerLazySingleton<Dio>(() {
    final dio = Dio();
    dio.interceptors.add(AuthInterceptor());
    return dio;
  });

  // Register API Client
  getIt.registerLazySingleton<ApiClient>(() => ApiClient(getIt<Dio>()));
}

Generating Code

Run the following command to generate the necessary code:

flutter pub run build_runner build --delete-conflicting-outputs

This will generate:

  • user.freezed.dart - Freezed model code
  • user.g.dart - JSON serialization code
  • api_client.g.dart - Retrofit client code

Using the API Client

Here’s how to use the API client in your application:

// Example usage in a repository or service class

class UserRepository {
  final ApiClient _apiClient;

  UserRepository(this._apiClient);

  Future<User> getUser(int id) async {
    try {
      return await _apiClient.getUser(id);
    } catch (e) {
      // Handle errors appropriately
      rethrow;
    }
  }
}

Best Practices

  1. Error Handling

    • Create custom error models
    • Implement global error handling in interceptors
    • Use try-catch blocks appropriately
  2. Response Mapping

    • Use Freezed’s @JsonSerializable for custom field mapping
    • Handle null values gracefully
    • Consider creating base response models
  3. Testing

    • Mock API responses for testing
    • Test error scenarios
    • Verify interceptor behavior

Common Issues and Solutions

  1. Code Generation Issues

    # If you encounter generation issues, try:
    flutter clean
    flutter pub get
    flutter pub run build_runner clean
    flutter pub run build_runner build --delete-conflicting-outputs
    
  2. Dio Configuration

    • Set appropriate timeouts
    • Configure logging for development
    • Handle certificate issues in development

Next Steps

  1. Implement error handling
  2. Add logging
  3. Set up environment-specific configurations
  4. Add authentication flow
  5. Implement caching strategy

For more information, refer to: