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

Как интегрировать REST API во Flutter приложение?

1.0 Junior🔥 262 комментариев
#Архитектура Flutter#Работа с сетью

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

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

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

Интеграция REST API во Flutter приложение

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

1. Базовое использование http пакета

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

class ApiClient {
  static const String baseUrl = 'https://api.example.com';
  
  // GET запрос
  Future<User> getUser(String userId) async {
    try {
      final response = await http.get(
        Uri.parse('$baseUrl/users/$userId'),
        headers: {'Content-Type': 'application/json'},
      ).timeout(Duration(seconds: 30));
      
      if (response.statusCode == 200) {
        return User.fromJson(jsonDecode(response.body));
      } else if (response.statusCode == 404) {
        throw Exception('Пользователь не найден');
      } else {
        throw Exception('Ошибка сервера: ${response.statusCode}');
      }
    } on TimeoutException {
      throw Exception('Timeout: сервер не отвечает');
    } catch (e) {
      throw Exception('Ошибка: $e');
    }
  }
  
  // POST запрос
  Future<User> createUser(CreateUserRequest request) async {
    final response = await http.post(
      Uri.parse('$baseUrl/users'),
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer $token',
      },
      body: jsonEncode(request.toJson()),
    );
    
    if (response.statusCode == 201) {
      return User.fromJson(jsonDecode(response.body));
    } else {
      throw Exception('Ошибка создания: ${response.body}');
    }
  }
  
  // PUT запрос
  Future<User> updateUser(String userId, UpdateUserRequest request) async {
    final response = await http.put(
      Uri.parse('$baseUrl/users/$userId'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode(request.toJson()),
    );
    
    if (response.statusCode == 200) {
      return User.fromJson(jsonDecode(response.body));
    } else {
      throw Exception('Ошибка обновления');
    }
  }
  
  // DELETE запрос
  Future<void> deleteUser(String userId) async {
    final response = await http.delete(
      Uri.parse('$baseUrl/users/$userId'),
    );
    
    if (response.statusCode != 204) {
      throw Exception('Ошибка удаления');
    }
  }
}

2. Архитектура с Dio (лучше)

Dio — это более мощный HTTP клиент с interceptors, retry логикой и т.д.

import 'package:dio/dio.dart';

class DioClient {
  late final Dio _dio;
  
  DioClient() {
    _dio = Dio(
      BaseOptions(
        baseUrl: 'https://api.example.com',
        connectTimeout: Duration(seconds: 30),
        receiveTimeout: Duration(seconds: 30),
        contentType: Headers.jsonContentType,
      ),
    );
    
    // Interceptor для логирования
    _dio.interceptors.add(
      LoggingInterceptor(),
    );
    
    // Interceptor для обработки ошибок аутентификации
    _dio.interceptors.add(
      AuthInterceptor(),
    );
    
    // Retry interceptor
    _dio.interceptors.add(
      RetryInterceptor(_dio),
    );
  }
  
  Future<User> getUser(String userId) async {
    try {
      final response = await _dio.get('/users/$userId');
      return User.fromJson(response.data);
    } on DioException catch (e) {
      _handleDioError(e);
      rethrow;
    }
  }
  
  Future<User> createUser(CreateUserRequest request) async {
    final response = await _dio.post(
      '/users',
      data: request.toJson(),
    );
    return User.fromJson(response.data);
  }
  
  void _handleDioError(DioException error) {
    switch (error.type) {
      case DioExceptionType.connectionTimeout:
        throw Exception('Connection timeout');
      case DioExceptionType.sendTimeout:
        throw Exception('Send timeout');
      case DioExceptionType.receiveTimeout:
        throw Exception('Receive timeout');
      case DioExceptionType.badResponse:
        throw Exception('Error: ${error.response?.statusCode}');
      default:
        throw Exception('Unknown error');
    }
  }
}

// Logging Interceptor
class LoggingInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    print('REQUEST[${options.method}] => PATH: ${options.path}');
    print('Headers: ${options.headers}');
    print('Body: ${options.data}');
    return super.onRequest(options, handler);
  }
  
  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    print('RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}');
    print('Data: ${response.data}');
    return super.onResponse(response, handler);
  }
  
  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    print('ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}');
    print('Error: ${err.error}');
    return super.onError(err, handler);
  }
}

// Auth Interceptor
class AuthInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    final token = await getStoredToken();
    if (token != null) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    return super.onRequest(options, handler);
  }
  
  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    // Если 401 Unauthorized, рефрешим токен
    if (err.response?.statusCode == 401) {
      await refreshToken();
      return handler.resolve(await _retry(err.requestOptions));
    }
    return super.onError(err, handler);
  }
}

3. Repository паттерн

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

class UserRepositoryImpl implements UserRepository {
  final DioClient dioClient;
  final UserLocalDataSource localDataSource;
  
  UserRepositoryImpl({
    required this.dioClient,
    required this.localDataSource,
  });
  
  @override
  Future<User> getUser(String userId) async {
    try {
      // Попытка получить с API
      final user = await dioClient.getUser(userId);
      // Сохранить локально для кэша
      await localDataSource.saveUser(user);
      return user;
    } catch (e) {
      // Вернуть кэшированные данные
      final cached = await localDataSource.getUser(userId);
      if (cached != null) {
        return cached;
      }
      rethrow;
    }
  }
  
  @override
  Future<List<User>> getUsers() async {
    try {
      final users = await dioClient.getUsers();
      await localDataSource.saveUsers(users);
      return users;
    } catch (e) {
      final cached = await localDataSource.getUsers();
      if (cached.isNotEmpty) {
        return cached;
      }
      rethrow;
    }
  }
  
  @override
  Future<void> createUser(CreateUserRequest request) async {
    await dioClient.createUser(request);
  }
}

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

abstract class UserEvent {}
class FetchUserEvent extends UserEvent {
  final String userId;
  FetchUserEvent({required this.userId});
}

abstract class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserSuccess extends UserState {
  final User user;
  UserSuccess({required this.user});
}
class UserError extends UserState {
  final String error;
  UserError({required this.error});
}

class UserBloc extends Bloc<UserEvent, UserState> {
  final UserRepository repository;
  
  UserBloc({required this.repository}) : super(UserInitial()) {
    on<FetchUserEvent>(_onFetchUser);
  }
  
  Future<void> _onFetchUser(
    FetchUserEvent event,
    Emitter<UserState> emit,
  ) async {
    emit(UserLoading());
    try {
      final user = await repository.getUser(event.userId);
      emit(UserSuccess(user: user));
    } catch (e) {
      emit(UserError(error: e.toString()));
    }
  }
}

// UI
class UserScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<UserBloc, UserState>(
      builder: (context, state) {
        if (state is UserLoading) {
          return CircularProgressIndicator();
        } else if (state is UserSuccess) {
          return Text('User: ${state.user.name}');
        } else if (state is UserError) {
          return Text('Error: ${state.error}');
        }
        return SizedBox();
      },
    );
  }
}

5. Обработка ошибок

class ApiException implements Exception {
  final String message;
  final int? statusCode;
  final dynamic originalError;
  
  ApiException({
    required this.message,
    this.statusCode,
    this.originalError,
  });
  
  @override
  String toString() => message;
}

String _getErrorMessage(dynamic error) {
  if (error is ApiException) {
    return error.message;
  } else if (error is DioException) {
    switch (error.type) {
      case DioExceptionType.badResponse:
        return 'Ошибка сервера: ${error.response?.statusCode}';
      case DioExceptionType.connectionTimeout:
        return 'Нет соединения';
      default:
        return 'Неизвестная ошибка';
    }
  }
  return error.toString();
}

6. Кэширование

class CachedRepository implements UserRepository {
  final UserRepository remote;
  final Duration cacheExpiration;
  final Map<String, CachedData<User>> _cache = {};
  
  CachedRepository({
    required this.remote,
    this.cacheExpiration = const Duration(hours: 1),
  });
  
  @override
  Future<User> getUser(String userId) async {
    // Проверить кэш
    if (_isCacheValid(userId)) {
      return _cache[userId]!.data;
    }
    
    // Получить с сервера
    final user = await remote.getUser(userId);
    
    // Сохранить в кэш
    _cache[userId] = CachedData(
      data: user,
      timestamp: DateTime.now(),
    );
    
    return user;
  }
  
  bool _isCacheValid(String userId) {
    if (!_cache.containsKey(userId)) return false;
    final cached = _cache[userId]!;
    final expired = DateTime.now().difference(cached.timestamp) > cacheExpiration;
    return !expired;
  }
}

class CachedData<T> {
  final T data;
  final DateTime timestamp;
  
  CachedData({required this.data, required this.timestamp});
}

Чеклист для production

  • Используй Dio вместо http
  • Обработай все ошибки (timeout, network, server)
  • Кэшируй данные локально
  • Добавь retry логику
  • Логируй запросы/ответы
  • Используй Token refresh
  • Валидируй JSON ответы
  • Установи timeout для запросов
  • Обработай 401/403 ошибки аутентификации
  • Используй Repository паттерн
# pubspec.yaml
dependencies:
  dio: ^5.0.0
  retrofit: ^4.0.0
  json_serializable: ^6.0.0
Как интегрировать REST API во Flutter приложение? | PrepBro