Back to Posts

Integrating Flutter with REST APIs

7 min read

Connecting to REST APIs is essential for dynamic apps. This article covers the basics of making HTTP requests, parsing JSON, and handling errors in Flutter.

Setting Up Dependencies

Add the required packages to your pubspec.yaml:

dependencies:
  http: ^0.13.4
  dio: ^4.0.0

Basic HTTP Requests

Using the http Package

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

Future<void> fetchData() async {
  final response = await http.get(
    Uri.parse('https://api.example.com/data'),
    headers: {'Authorization': 'Bearer your_token'},
  );

  if (response.statusCode == 200) {
    // Parse the response
    final data = jsonDecode(response.body);
    print(data);
  } else {
    throw Exception('Failed to load data');
  }
}

Using the dio Package

import 'package:dio/dio.dart';

final dio = Dio();

Future<void> fetchData() async {
  try {
    final response = await dio.get(
      'https://api.example.com/data',
      options: Options(
        headers: {'Authorization': 'Bearer your_token'},
      ),
    );
    print(response.data);
  } catch (e) {
    print('Error: $e');
  }
}

JSON Serialization

Manual JSON Parsing

class User {
  final int id;
  final String name;
  final String email;

  User({
    required this.id,
    required this.name,
    required this.email,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'],
      name: json['name'],
      email: json['email'],
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
    };
  }
}

Using json_serializable

Add to pubspec.yaml:

dependencies:
  json_annotation: ^4.0.0

dev_dependencies:
  build_runner: ^2.0.0
  json_serializable: ^4.0.0

Create the model:

import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';

@JsonSerializable()
class User {
  final int id;
  final String name;
  final String email;

  User({
    required this.id,
    required this.name,
    required this.email,
  });

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

Run the build runner:

flutter pub run build_runner build

Error Handling

Basic Error Handling

Future<User> fetchUser(int id) async {
  try {
    final response = await dio.get('/users/$id');
    return User.fromJson(response.data);
  } on DioError catch (e) {
    if (e.response?.statusCode == 404) {
      throw UserNotFoundException();
    } else if (e.type == DioErrorType.connectTimeout) {
      throw NetworkException();
    } else {
      throw UnknownException();
    }
  }
}

Custom Exception Classes

class UserNotFoundException implements Exception {
  final String message = 'User not found';
}

class NetworkException implements Exception {
  final String message = 'Network error occurred';
}

class UnknownException implements Exception {
  final String message = 'An unknown error occurred';
}

API Service Layer

Create a service class to handle API calls:

class ApiService {
  final Dio _dio;

  ApiService() : _dio = Dio() {
    _dio.options.baseUrl = 'https://api.example.com';
    _dio.interceptors.add(LogInterceptor());
  }

  Future<List<User>> getUsers() async {
    try {
      final response = await _dio.get('/users');
      return (response.data as List)
          .map((json) => User.fromJson(json))
          .toList();
    } catch (e) {
      throw _handleError(e);
    }
  }

  Future<User> createUser(User user) async {
    try {
      final response = await _dio.post(
        '/users',
        data: user.toJson(),
      );
      return User.fromJson(response.data);
    } catch (e) {
      throw _handleError(e);
    }
  }

  Exception _handleError(dynamic error) {
    if (error is DioError) {
      switch (error.type) {
        case DioErrorType.connectTimeout:
        case DioErrorType.sendTimeout:
        case DioErrorType.receiveTimeout:
          return NetworkException();
        case DioErrorType.response:
          if (error.response?.statusCode == 404) {
            return UserNotFoundException();
          }
          return UnknownException();
        default:
          return UnknownException();
      }
    }
    return UnknownException();
  }
}

State Management with API Calls

Using Provider

class UserProvider extends ChangeNotifier {
  final ApiService _apiService;
  List<User> _users = [];
  bool _isLoading = false;
  String? _error;

  List<User> get users => _users;
  bool get isLoading => _isLoading;
  String? get error => _error;

  UserProvider(this._apiService);

  Future<void> fetchUsers() async {
    _isLoading = true;
    _error = null;
    notifyListeners();

    try {
      _users = await _apiService.getUsers();
    } catch (e) {
      _error = e.toString();
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

Testing API Integration

void main() {
  group('ApiService Tests', () {
    late ApiService apiService;
    late MockDio mockDio;

    setUp(() {
      mockDio = MockDio();
      apiService = ApiService(mockDio);
    });

    test('getUsers returns list of users', () async {
      // Arrange
      final mockUsers = [
        {'id': 1, 'name': 'John', 'email': 'john@example.com'},
        {'id': 2, 'name': 'Jane', 'email': 'jane@example.com'},
      ];
      when(mockDio.get('/users')).thenAnswer(
        (_) async => Response(
          data: mockUsers,
          statusCode: 200,
          requestOptions: RequestOptions(path: '/users'),
        ),
      );

      // Act
      final users = await apiService.getUsers();

      // Assert
      expect(users.length, 2);
      expect(users[0].name, 'John');
      expect(users[1].name, 'Jane');
    });
  });
}

Best Practices

  1. Use a Service Layer: Separate API calls from UI logic
  2. Implement Error Handling: Handle all possible error cases
  3. Add Loading States: Show loading indicators during API calls
  4. Cache Responses: Implement caching for better performance
  5. Use Interceptors: Add logging, authentication, etc.
  6. Test Thoroughly: Write unit and integration tests
  7. Handle Offline Mode: Implement offline support

Conclusion

Integrating REST APIs in Flutter involves:

  • Setting up HTTP clients
  • Implementing JSON serialization
  • Creating a service layer
  • Handling errors and loading states
  • Testing the integration
  • Following best practices

By following these guidelines, you can create robust and maintainable API integrations in your Flutter applications.