Комментарии (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>()));
});
});
}
Лучшие практики, которые использую
-
Separation of Concerns
- ApiService — чистый HTTP клиент
- Repository — бизнес-логика работы с данными
- UI — только отображение
-
Error Handling
- Специфичные исключения для разных ошибок
- Retry логика для временных ошибок
- User-friendly сообщения в UI
-
Performance
- Кэширование для снижения нагрузки
- Timeout для избежания зависания
- Batch requests где возможно
-
Security
- HTTPS everywhere
- Secure storage для токенов
- Валидация входных данных
- Санитизация вывода
-
Testability
- Mock'и для тестирования
- Dependency injection
- Абстрактные interfaces
-
Maintainability
- Типизирование моделей
- Ясная структура кода
- Документирование API endpoints
Этот подход позволяет строить надёжные, поддерживаемые и тестируемые приложения с качественным взаимодействием с API.