Рубрики
Flutter

REST API во Flutter: Полное руководство 2025

Полное руководство по работе с REST API во Flutter: http пакет, dio, retrofit, error handling, caching.

Большинство Flutter приложений работают с сервером через REST API. Разберём лучшие практики работы с API.

HTTP пакет

Базовый запрос

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

Future<void> fetchData() async {
  final response = await http.get(
    Uri.parse('https://api.example.com/users'),
  );

  if (response.statusCode == 200) {
    final data = json.decode(response.body);
    print(data);
  }
}

POST запрос

Future<void> createUser(String name, String email) async {
  final response = await http.post(
    Uri.parse('https://api.example.com/users'),
    headers: {
      'Content-Type': 'application/json',
    },
    body: json.encode({
      'name': name,
      'email': email,
    }),
  );

  if (response.statusCode == 201) {
    print('User created');
  }
}

PUT и DELETE

Future<void> updateUser(String id, String name) async {
  final response = await http.put(
    Uri.parse('https://api.example.com/users/$id'),
    headers: {'Content-Type': 'application/json'},
    body: json.encode({'name': name}),
  );
}

Future<void> deleteUser(String id) async {
  final response = await http.delete(
    Uri.parse('https://api.example.com/users/$id'),
  );
}

Dio пакет

Установка

dependencies:
  dio: ^5.4.0

Базовая настройка

import 'package:dio/dio.dart';

class ApiService {
  late final Dio _dio;

  ApiService() {
    _dio = Dio(
      BaseOptions(
        baseUrl: 'https://api.example.com',
        connectTimeout: Duration(seconds: 5),
        receiveTimeout: Duration(seconds: 3),
      ),
    );

    _setupInterceptors();
  }

  void _setupInterceptors() {
    _dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (options, handler) {
          // Добавить токен
          final token = storage.getToken();
          if (token != null) {
            options.headers['Authorization'] = 'Bearer $token';
          }
          return handler.next(options);
        },
        onResponse: (response, handler) {
          return handler.next(response);
        },
        onError: (error, handler) {
          // Обработка ошибок
          if (error.response?.statusCode == 401) {
            // Обновить токен
          }
          return handler.next(error);
        },
      ),
    );
  }
}

GET запросы

Future<List<User>> getUsers() async {
  try {
    final response = await _dio.get('/users');

    final List<dynamic> data = response.data;

    return data.map((json) => User.fromJson(json)).toList();
  } catch (e) {
    throw Exception('Failed to load users: $e');
  }
}

Future<User?> getUser(String id) async {
  try {
    final response = await _dio.get('/users/$id');

    return User.fromJson(response.data);
  } catch (e) {
    return null;
  }
}

POST запрос

Future<User> createUser(CreateUserRequest request) async {
  try {
    final response = await _dio.post(
      '/users',
      data: request.toJson(),
    );

    return User.fromJson(response.data);
  } catch (e) {
    throw Exception('Failed to create user: $e');
  }
}

Загрузка файлов

Future<String> uploadFile(File file) async {
  try {
    final formData = FormData.fromMap({
      'file': await MultipartFile.fromFile(file.path),
    });

    final response = await _dio.post(
      '/upload',
      data: formData,
    );

    return response.data['url'];
  } catch (e) {
    throw Exception('Failed to upload file: $e');
  }
}

Download файлов

Future<void> downloadFile(String url, String savePath) async {
  try {
    await _dio.download(
      url,
      savePath,
      onReceiveProgress: (received, total) {
        final progress = (received / total) * 100;
        print('Downloading: $progress%');
      },
    );
  } catch (e) {
    throw Exception('Failed to download: $e');
  }
}

Retrofit

Установка

dependencies:
  retrofit: ^4.0.0
  json_annotation: ^4.9.0
dev_dependencies:
  retrofit_generator: ^8.0.0
  build_runner: ^2.4.0
  json_serializable: ^6.8.0

API интерфейс

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

part 'api_service.g.dart';

@RestApi(baseUrl: 'https://api.example.com')
abstract class ApiService {
  factory ApiService(Dio dio) = _ApiService;

  @GET('/users')
  Future<List<User>> getUsers();

  @GET('/users/{id}')
  Future<User> getUser(@Path('id') String id);

  @POST('/users')
  Future<User> createUser(@Body() CreateUserRequest request);

  @PUT('/users/{id}')
  Future<User> updateUser(
    @Path('id') String id,
    @Body() UpdateUserRequest request,
  );

  @DELETE('/users/{id}')
  Future<void> deleteUser(@Path('id') String id);

  @POST('/upload')
  Future<String> uploadFile(@Part() File file);

  @GET('/search')
  Future<List<Item>> searchItems(
    @Query('q') String query,
    @Query('page') int page,
    @Query('limit') int limit,
  );
}

Использование

final dio = Dio();
final apiService = ApiService(dio);

final users = await apiService.getUsers();
final user = await apiService.getUser('123');
final newUser = await apiService.createUser(
  CreateUserRequest(name: 'John', email: 'john@example.com'),
);

Error Handling

Custom exceptions

abstract class AppException implements Exception {
  final String message;
  final int? statusCode;

  AppException(this.message, {this.statusCode});

  @override
  String toString() => message;
}

class NetworkException extends AppException {
  NetworkException(String message) : super(message);
}

class ServerException extends AppException {
  ServerException(String message, {int? statusCode})
      : super(message, statusCode: statusCode);
}

class AuthException extends AppException {
  AuthException(String message) : super(message);
}

class NotFoundException extends AppException {
  NotFoundException(String message) : super(message, statusCode: 404);
}

Error handler

class ErrorHandler {
  static AppException handleError(dynamic error) {
    if (error is DioException) {
      switch (error.type) {
        case DioExceptionType.connectionTimeout:
        case DioExceptionType.sendTimeout:
        case DioExceptionType.receiveTimeout:
          return NetworkException('Connection timeout');

        case DioExceptionType.badResponse:
          final statusCode = error.response?.statusCode;

          switch (statusCode) {
            case 401:
              return AuthException('Unauthorized');
            case 404:
              return NotFoundException('Resource not found');
            case 500:
              return ServerException('Internal server error', statusCode: 500);
            default:
              return ServerException(
                'Request failed with status $statusCode',
                statusCode: statusCode,
              );
          }

        case DioExceptionType.cancel:
          return NetworkException('Request cancelled');

        case DioExceptionType.connectionError:
          return NetworkException('No internet connection');

        default:
          return AppException('Unexpected error occurred');
      }
    }

    return AppException(error.toString());
  }
}

Caching

HTTP Cache

dependencies:
  dio_cache_interceptor: ^3.4.0
final cacheOptions = CacheOptions(
  store: MemCacheStore(),
  policy: CachePolicy.request,
  hitCacheOnErrorExcept: [401, 403],
  maxStale: Duration(days: 7),
);

final dio = Dio();
dio.interceptors.add(DioCacheInterceptor(options: cacheOptions));

Локальное кэширование

class CachedApiService {
  final ApiService _api;
  final Map<String, CachedData> _cache = {};

  CachedApiService(this._api);

  Future<T> get<T>(
    String key,
    Future<T> Function() fetch, {
    Duration duration = const Duration(minutes: 5),
  }) async {
    // Проверка кэша
    if (_cache.containsKey(key)) {
      final cached = _cache[key]!;

      if (!cached.isExpired) {
        return cached.data as T;
      }

      _cache.remove(key);
    }

    // Загрузка данных
    final data = await fetch();

    // Сохранение в кэш
    _cache[key] = CachedData(
      data: data,
      expiry: DateTime.now().add(duration),
    );

    return data;
  }

  void invalidate(String key) {
    _cache.remove(key);
  }

  void clear() {
    _cache.clear();
  }
}

class CachedData<T> {
  final T data;
  final DateTime expiry;

  CachedData({required this.data, required this.expiry});

  bool get isExpired => DateTime.now().isAfter(expiry);
}

Repository Pattern

abstract class UserRepository {
  Future<List<User>> getUsers();
  Future<User?> getUser(String id);
  Future<User> createUser(CreateUserRequest request);
}

class UserRepositoryImpl implements UserRepository {
  final ApiService _api;
  final CacheService _cache;

  UserRepositoryImpl(this._api, this._cache);

  @override
  Future<List<User>> getUsers() async {
    try {
      return await _api.getUsers();
    } catch (e) {
      throw ErrorHandler.handleError(e);
    }
  }

  @override
  Future<User?> getUser(String id) async {
    try {
      return await _cache.get(
        'user_$id',
        () => _api.getUser(id),
        duration: Duration(minutes: 10),
      );
    } catch (e) {
      throw ErrorHandler.handleError(e);
    }
  }

  @override
  Future<User> createUser(CreateUserRequest request) async {
    try {
      final user = await _api.createUser(request);

      // Инвалидация кэша
      _cache.clear();

      return user;
    } catch (e) {
      throw ErrorHandler.handleError(e);
    }
  }
}

Заключение

Работа с REST API во Flutter — это важная часть разработки. Используйте Dio для сложных сценариев и Retrofit для типобезопасности.