← Назад к вопросам

Как реализовывал взаимодействие с API?

2.0 Middle🔥 161 комментариев
#Работа с сетью

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Взаимодействие с API в Flutter

Взаимодействие с API — это критически важная часть любого мобильного приложения. За годы я выработал структурированный подход к организации API слоя, который обеспечивает надёжность, масштабируемость и удобство тестирования.

Архитектура: Repository Pattern

Я использую классический паттерн Repository, который отделяет бизнес-логику от работы с данными:

// 1. Модели данных
class User {
  final int id;
  final String name;
  final String email;
  
  User({
    required this.id,
    required this.name,
    required this.email,
  });
  
  // JSON сериализация
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'],
      name: json['name'],
      email: json['email'],
    );
  }
  
  Map<String, dynamic> toJson() => {
    'id': id,
    'name': name,
    'email': email,
  };
}

// 2. API Service — низкоуровневая работа с HTTP
class ApiService {
  final String baseUrl;
  late final http.Client _httpClient;
  
  ApiService({required this.baseUrl}) {
    _httpClient = http.Client();
  }
  
  Future<dynamic> get(String endpoint) async {
    try {
      final response = await _httpClient
        .get(
          Uri.parse('$baseUrl$endpoint'),
          headers: {'Content-Type': 'application/json'},
        )
        .timeout(Duration(seconds: 30));
      
      return _handleResponse(response);
    } catch (e) {
      throw ApiException('GET request failed: $e');
    }
  }
  
  Future<dynamic> post(String endpoint, {required dynamic body}) async {
    try {
      final response = await _httpClient
        .post(
          Uri.parse('$baseUrl$endpoint'),
          headers: {'Content-Type': 'application/json'},
          body: jsonEncode(body),
        )
        .timeout(Duration(seconds: 30));
      
      return _handleResponse(response);
    } catch (e) {
      throw ApiException('POST request failed: $e');
    }
  }
  
  dynamic _handleResponse(http.Response response) {
    if (response.statusCode == 200 || response.statusCode == 201) {
      return jsonDecode(response.body);
    } else if (response.statusCode == 400) {
      throw BadRequestException('Bad request: ${response.body}');
    } else if (response.statusCode == 401) {
      throw UnauthorizedException('Unauthorized');
    } else if (response.statusCode == 500) {
      throw ServerException('Server error');
    } else {
      throw ApiException('Unknown error: ${response.statusCode}');
    }
  }
  
  void dispose() {
    _httpClient.close();
  }
}

// 3. Custom Exceptions
class ApiException implements Exception {
  final String message;
  ApiException(this.message);
  
  @override
  String toString() => message;
}

class BadRequestException extends ApiException {
  BadRequestException(String message) : super(message);
}

class UnauthorizedException extends ApiException {
  UnauthorizedException(String message) : super(message);
}

class ServerException extends ApiException {
  ServerException(String message) : super(message);
}

// 4. Repository — слой абстракции
abstract class UserRepository {
  Future<User> getUser(int id);
  Future<List<User>> getAllUsers();
  Future<User> createUser(String name, String email);
  Future<void> deleteUser(int id);
}

class UserRepositoryImpl implements UserRepository {
  final ApiService _apiService;
  
  UserRepositoryImpl({required ApiService apiService})
    : _apiService = apiService;
  
  @override
  Future<User> getUser(int id) async {
    try {
      final response = await _apiService.get('/users/$id');
      return User.fromJson(response);
    } on ApiException {
      rethrow;
    }
  }
  
  @override
  Future<List<User>> getAllUsers() async {
    try {
      final response = await _apiService.get('/users');
      return (response as List)
        .map((json) => User.fromJson(json))
        .toList();
    } on ApiException {
      rethrow;
    }
  }
  
  @override
  Future<User> createUser(String name, String email) async {
    try {
      final response = await _apiService.post(
        '/users',
        body: {'name': name, 'email': email},
      );
      return User.fromJson(response);
    } on ApiException {
      rethrow;
    }
  }
  
  @override
  Future<void> deleteUser(int id) async {
    try {
      await _apiService.get('/users/$id/delete');
    } on ApiException {
      rethrow;
    }
  }
}

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

// С использованием Provider
final userRepositoryProvider = Provider<UserRepository>((ref) {
  final apiService = ApiService(baseUrl: 'https://api.example.com');
  return UserRepositoryImpl(apiService: apiService);
});

final userProvider = FutureProvider<User>((ref) {
  final repository = ref.watch(userRepositoryProvider);
  return repository.getUser(1);
});

// UI Widget
class UserScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsyncValue = ref.watch(userProvider);
    
    return userAsyncValue.when(
      data: (user) => UserCard(user: user),
      loading: () => LoadingWidget(),
      error: (error, stack) => ErrorWidget(error: error.toString()),
    );
  }
}

Обработка ошибок и retry логика

class ResilientApiService extends ApiService {
  static const maxRetries = 3;
  
  @override
  Future<dynamic> get(String endpoint) async {
    for (int attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        return await super.get(endpoint);
      } catch (e) {
        if (attempt == maxRetries) {
          rethrow;
        }
        // Экспоненциальная задержка: 1s, 2s, 4s
        await Future.delayed(Duration(seconds: 1 << (attempt - 1)));
      }
    }
  }
}

Кэширование API ответов

class CachedApiService extends ApiService {
  final Map<String, CacheEntry> _cache = {};
  final Duration _cacheDuration;
  
  CachedApiService({
    required String baseUrl,
    this._cacheDuration = const Duration(minutes: 5),
  }) : super(baseUrl: baseUrl);
  
  @override
  Future<dynamic> get(String endpoint) async {
    // Проверяем кэш
    if (_cache.containsKey(endpoint)) {
      final entry = _cache[endpoint]!;
      if (DateTime.now().isBefore(entry.expiresAt)) {
        return entry.data;
      } else {
        _cache.remove(endpoint);
      }
    }
    
    // Запрос к API
    final response = await super.get(endpoint);
    
    // Сохраняем в кэш
    _cache[endpoint] = CacheEntry(
      data: response,
      expiresAt: DateTime.now().add(_cacheDuration),
    );
    
    return response;
  }
  
  void clearCache() => _cache.clear();
}

class CacheEntry {
  final dynamic data;
  final DateTime expiresAt;
  
  CacheEntry({required this.data, required this.expiresAt});
}

Авторизация и токены

class AuthService {
  String? _accessToken;
  String? _refreshToken;
  
  bool get isAuthenticated => _accessToken != null;
  
  Future<void> login(String email, String password) async {
    // Получаем токены
    final response = await http.post(
      Uri.parse('${ApiService.baseUrl}/auth/login'),
      body: {'email': email, 'password': password},
    );
    
    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      _accessToken = data['access_token'];
      _refreshToken = data['refresh_token'];
      // Сохраняем токены в secure storage
      await _saveTokens();
    }
  }
  
  Future<String?> getAccessToken() async {
    // Проверяем, не истёк ли токен, и обновляем если нужно
    return _accessToken;
  }
  
  Future<void> logout() async {
    _accessToken = null;
    _refreshToken = null;
    await _clearTokens();
  }
  
  Future<void> _saveTokens() async {
    // Сохранение в flutter_secure_storage
  }
  
  Future<void> _clearTokens() async {
    // Удаление из secure storage
  }
}

// Интеграция с API Service
class AuthenticatedApiService extends ApiService {
  final AuthService _authService;
  
  AuthenticatedApiService({
    required String baseUrl,
    required AuthService authService,
  }) : _authService = authService, super(baseUrl: baseUrl);
  
  @override
  Future<dynamic> get(String endpoint) async {
    final token = await _authService.getAccessToken();
    // Добавляем токен в заголовок
    // ...
    return super.get(endpoint);
  }
}

Тестирование API слоя

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

class MockHttpClient extends Mock implements http.Client {}

void main() {
  group('UserRepository', () {
    late UserRepository repository;
    late MockHttpClient mockHttpClient;
    
    setUp(() {
      mockHttpClient = MockHttpClient();
      final apiService = ApiService(baseUrl: 'https://api.example.com');
      repository = UserRepositoryImpl(apiService: apiService);
    });
    
    test('getUser returns user on success', () async {
      // Arrange
      when(mockHttpClient.get(
        Uri.parse('https://api.example.com/users/1'),
        headers: anyNamed('headers'),
      )).thenAnswer((_) async => http.Response(
        jsonEncode({'id': 1, 'name': 'John', 'email': 'john@example.com'}),
        200,
      ));
      
      // Act
      final user = await repository.getUser(1);
      
      // Assert
      expect(user.id, 1);
      expect(user.name, 'John');
    });
    
    test('getUser throws on error', () async {
      // Arrange
      when(mockHttpClient.get(
        Uri.parse('https://api.example.com/users/999'),
        headers: anyNamed('headers'),
      )).thenAnswer((_) async => http.Response('Not found', 404));
      
      // Assert
      expect(() => repository.getUser(999), throwsA(isA<ApiException>()));
    });
  });
}

Лучшие практики, которые использую

  1. Separation of Concerns

    • ApiService — чистый HTTP клиент
    • Repository — бизнес-логика работы с данными
    • UI — только отображение
  2. Error Handling

    • Специфичные исключения для разных ошибок
    • Retry логика для временных ошибок
    • User-friendly сообщения в UI
  3. Performance

    • Кэширование для снижения нагрузки
    • Timeout для избежания зависания
    • Batch requests где возможно
  4. Security

    • HTTPS everywhere
    • Secure storage для токенов
    • Валидация входных данных
    • Санитизация вывода
  5. Testability

    • Mock'и для тестирования
    • Dependency injection
    • Абстрактные interfaces
  6. Maintainability

    • Типизирование моделей
    • Ясная структура кода
    • Документирование API endpoints

Этот подход позволяет строить надёжные, поддерживаемые и тестируемые приложения с качественным взаимодействием с API.

Как реализовывал взаимодействие с API? | PrepBro