Ранее мы разобрали основы клиент-серверного взаимодействия и научились работе со стандартным пакетом http.
В этой статье мы рассмотрим три более продвинутых пакета для Flutter — Dio, Retrofit и Chopper. У каждого из них свои особенности, преимущества и недостатки, которые мы разберём подробнее.
Dio
Одна из самых популярных библиотек для работы с сетью — это пакет Dio. HTTP-клиент для Dart/Flutter, который поддерживает глобальные настройки, передачу файлов, интерцепторы, тайм-ауты, отмены запросов и многое другое. По функциональности Dio в несколько раз превосходит стандартный пакет http, предоставляя более гибкие возможности для взаимодействия с сетью.
Установка
Для начала работы с Dio необходимо добавить следующую зависимость в pubspec.yaml
:
1dependencies:
2 flutter:
3 sdk: flutter
4 dio: <latest_version>
После добавления зависимости выполните команду flutter pub get
в терминале для установки пакета.
Использование
Шаг 1: Создание модели данных
Перед началом взаимодействия необходимо создать объекты, которые мы будем использовать для отправки и получения данных. В качестве примера давайте создадим класс User
с полями name
и email
. Для его реализации мы будем использовать пакет json_serializable
, установку и настройку которого мы разбирали подробнее в статье про работу с сетью.
1import 'package:json_annotation/json_annotation.dart';
2
3part 'user.g.dart';
4
5@JsonSerializable()
6class User {
7 final String name;
8 final String email;
9 User({required this.name, required this.email});
10
11 factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
12 Map<String, dynamic> toJson() => _$UserToJson(this);
13}
После создания класса выполните команду flutter pub run build_runner build
для генерации кода JSON.
Шаг 2: Создание класса API
Следующим шагом необходимо создать файл, где мы определим точки доступа к нашему API, используя пакет Dio. Давайте создадим класс ApiService
и реализуем методы getUser
и createUser
, которые будут отвечать за получение и создание пользователя.
1import 'package:dio/dio.dart';
2import 'user.dart';
3
4class ApiService {
5 final Dio _dio;
6
7 ApiService(this._dio);
8
9 /// Метод получения пользователя
10 Future<User> getUser(String id) {
11 try {
12 final response = await dio.get('https://api.mysite.ru/users/$id');
13 return User.fromJson(response.data);
14 } on DioException catch (e) {
15 print('При выполнении запроса возникла ошибка: ${e.message}');
16 rethrow;
17 }
18 }
19
20 /// Метод создания пользователя
21 Future<User> createUser(User newUser) {
22 try {
23 final response = await dio.post('https://api.mysite.ru/users/create', data: newUser.toJson());
24 return User.fromJson(response.data);
25 } on DioException catch (e) {
26 print('При выполнении запроса возникла ошибка: ${e.message}');
27 rethrow;
28 }
29 }
30}
Шаг 3: Выполнение запросов
Чтобы использовать получившийся ApiService
, создадим экземпляр Dio и передадим его в конструктор. После этого мы можем вызывать методы getUser
и createUser
для выполнения сетевого взаимодействия и обработки полученных данных в виде объекта класса User
.
1// Создание экземпляра Dio
2final dio = Dio();
3
4// Создание экземпляра ApiService
5final apiService = ApiService(dio);
6
7// Отправка запроса на получение пользователя
8final user = await apiService.getUser('123');
9print(user.name);
10
11// Отправка запроса на создание пользователя
12final newUser = User(name: 'Alex', email: 'alex@ya.ru');
13final createdUser = await apiService.createUser(newUser);
14print(createdUser.id);
Описание функционала
Базовые опции
BaseOptions
— это конфигурационный объект, который позволяет задавать настройки по умолчанию для всех HTTP-запросов, выполняемых с использованием Dio. Это включает в себя настройку таких параметров, как базовый URL, заголовки, тайм-ауты и многое другое. Настройка BaseOptions
помогает сделать код более управляемым и избежать дублирования при конфигурировании каждого запроса отдельно.
Основные параметры BaseOptions
:
- baseUrl. Строка, указывающая базовый URL, который будет использоваться как префикс для всех URL, указанных в запросах.
- connectTimeout. Время в миллисекундах, после которого запрос будет отменён, если подключение с сервером не будет установлено.
- receiveTimeout. Время в миллисекундах, после которого запрос будет отменён, если от сервера не начнут поступать данные.
- sendTimeout. Время в миллисекундах, после которого запрос будет отменён, если данные не будут отправлены на сервер.
- headers. Заголовки, которые будут установлены для каждого запроса.
- contentType. Тип содержимого тела запроса, который будет отправлен на сервер.
- responseType. Тип содержимого тела ответа, который ожидается получить от сервера.
Пример настройки и использования BaseOptions
:
1BaseOptions options = BaseOptions(
2 baseUrl: 'https://api.mysite.ru/', // Базовый URL для всех запросов
3 connectTimeout: 10000, // Тайм-аут подключения в миллисекундах
4 receiveTimeout: 7000, // Тайм-аут получения данных в миллисекундах
5 sendTimeout: 5000, // Тайм-аут отправки данных в миллисекундах
6 headers: {
7 'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
8 'Other-Header': 'value'
9 }, // Заголовки по умолчанию для всех запросов
10 contentType: 'application/json', // Тип содержимого по умолчанию
11 responseType: ResponseType.json, // Тип ожидаемого ответа
12);
13
14// Создание экземпляра Dio с предустановленными настройками
15Dio dio = Dio(options);
Запросы
Пакет Dio предоставляет обширное разнообразие методов, которые позволяют выполнять стандартные HTTP-запросы, такие как GET, POST, DELETE, PUT и другие. Давайте рассмотрим два основных метода GET и POST вместе с их параметрами.
Метод GET
Используется для получения данных с сервера.
1Future<Response<T>> get<T>(
2 String path, {
3 Map<String, dynamic>? queryParameters,
4 Options? options,
5 CancelToken? cancelToken,
6 ProgressCallback? onReceiveProgress,
7});
Параметры метода GET
:
- path. URL-адрес (или путь) ресурса, к которому вы хотите обратиться. Этот путь может быть как полным URL, так и относительным, если базовый URL уже задан в настройках Dio.
- queryParameters. Параметры запроса. Если вам нужно передать в запрос данные, такие как фильтры или токены, в форме «ключ — значение», вы можете использовать queryParameters. Например, для запроса страницы или фильтрации по определённым критериям.
- options. Дополнительные опции для конкретного запроса. Options может содержать заголовки, время ожидания запроса, responseType и другие параметры, которые переопределяют глобальные настройки Dio для этого конкретного запроса.
- cancelToken. Механизм отмены запроса. Используется для отмены HTTP-запросов, которые больше не нужны. Например, если пользователь закрыл страницу, где данные всё ещё загружаются.
- onReceiveProgress. Коллбэк прогресса получения данных. Это может быть полезно, если вы загружаете большие объёмы данных и хотите показать пользователю индикатор прогресса.
Пример настройки и использования метода GET
:
1final response = await dio.get(
2 'https://api.mysite.ru/users',
3 queryParameters: {'id': '123'},
4 options: Options(headers: {'Authorization': 'Bearer YOUR_ACCESS_TOKEN'}),
5 onReceiveProgress: (int received, int total) {
6 print('Получено $received байт из $total байт');
7 },
8);
9
10final user = User.fromJson(response.data);
11print(user.name);
Метод POST
Используется для отправки данных на сервер.
1Future<Response<T>> post<T>(
2 String path, {
3 data,
4 Map<String, dynamic>? queryParameters,
5 Options? options,
6 CancelToken? cancelToken,
7 ProgressCallback? onSendProgress,
8 ProgressCallback? onReceiveProgress,
9});
Параметры метода POST
:
- data. Данные, которые нужно отправить на сервер. Это может быть простой объект, такой как
Map
,List
,String
, или дажеFormData
для отправки файлов и сложных объектов. - onReceiveProgress. Коллбэк отправки данных на сервер. Это полезно для отображения прогресса отправки, если вы загружаете большие файлы или данные.
Остальные параметры повторяют поведение, которое было описано для метода GET.
Пример настройки и использования метода POST
:
1final response = await dio.post(
2 'https://api.mysite.ru/users/create',
3 data: {'name': 'Alex', 'email': 'alex@ya.ru'},
4 options: Options(headers: {'Authorization': 'Bearer YOUR_ACCESS_TOKEN'}),
5 onSendProgress: (int sent, int total) {
6 print('Отправлено $sent байт из $total байт');
7 },
8);
9
10final newUser = User.fromJson(response.data);
11print(newUser.id);
Ответы
Пакет Dio предоставляет класс Response, который содержит HTTP-ответ, полученный от сервера после выполнения запроса.
Основные поля класса Response
:
- data. Тело ответа; может быть в различных форматах, таких как строка,
Map
,List
, илиbytes
, в зависимости от заданного responseType и типа содержимого ответа. - headers. Заголовки ответа; могут содержать полезную информацию, такую как cookies, тип контента, длина содержимого и другие метаданные.
- requestOptions. Содержит экземпляр RequestOptions, который описывает запрос, приведший к данному ответу, включая URL, заголовки, параметры запроса, настройки тайм-аута и др.
- statusCode. Код состояния HTTP-ответа (например, 200, 404 и т. д.). Статусный код позволяет легко определить, был запрос успешен или нет.
- statusMessage. Сообщение состояния ответа HTTP. Это описание кода состояния, предоставленное сервером.
- isRedirect. Булево значение, указывающее, было ли перенаправление в процессе выполнения запроса.
- redirects. Список перенаправлений, которые были выполнены в процессе запроса.
- extra. Предоставляет место для хранения дополнительных данных, связанных с ответом. Можно использовать для сохранения информации, специфичной для запроса или ответа.
Пример получения и использования Response
:
1try {
2 final response = await dio.get('https://api.mysite.ru/users/123');
3 print('Код состояния: ${response.statusCode}');
4 print('Тело ответа: ${response.data}');
5
6 if (response.statusCode == 200) {
7 // Обработка успешного ответа
8 var data = response.data;
9 // Действия с данными
10 }
11} on DioException catch (e) {
12 if (e.response != null) {
13 // Это ошибка, при которой сервер вернул ответ с ошибкой
14 print('Код ошибки: ${e.response!.statusCode}');
15 print('Данные ошибки: ${e.response!.data}');
16 } else {
17 // Ошибка запроса или отмена запроса
18 print('При выполнении запроса возникла ошибка: ${e.message}');
19 }
20}
Дополнительные возможности
Интерцепторы
Интерцепторы в пакете Dio представляют собой мощный инструмент для перехвата и обработки HTTP-запросов и ответов перед их отправкой на сервер или после получения от сервера соответственно. Интерцепторы могут быть использованы для различных целей, таких как логирование, добавление общих заголовков, обработка ошибок, предварительная обработка данных и других.
Типы интерцепторов:
-
Request Interceptor. Интерцептор запросов. Используется для изменения запроса перед его отправкой. Это полезно для установки или изменения заголовков, токенов аутентификации, логирования и т. д.
-
Response Interceptor. Интерцептор ответов. Используется после получения ответа от сервера, но перед его обработкой в сервисе. Может быть использовано для обработки или преобразования данных ответа, централизованной обработки ошибок и т. д.
-
Error Interceptor. Интерцептор ошибок. Используется для перехвата и обработки ошибок, возникающих во время выполнения HTTP-запросов. Идеально подходит, чтобы обработать ошибки и, возможно, попытаться исправить их или повторить запрос.
Пример настройки и использования интерцепторов:
1dio.interceptors.add(InterceptorsWrapper(
2 onRequest: (RequestOptions options, RequestInterceptorHandler handler) {
3 // Добавление токена аутентификации
4 options.headers['Authorization'] = 'Bearer YOUR_ACCESS_TOKEN';
5 print('Отправка запроса ${options.uri}');
6 return handler.next(options);
7 },
8 onResponse: (Response response, ResponseInterceptorHandler handler) {
9 // Вывод кода статуса ответа
10 print('Статус ответа: ${response.statusCode}');
11 return handler.next(response);
12 },
13 onError: (DioException e, ErrorInterceptorHandler handler) {
14 print('Ошибка: ${e.message}');
15 if (exception.response?.statusCode == 401) {
16 // Обработка конкретного кода ошибки (например, истекла сессия)
17 print('Запрос не авторизован');
18 }
19 return handler.next(exception);
20 },
21));
22
23// Теперь каждый запрос через этот экземпляр Dio будет обрабатываться интерцепторами
24dio.get('https://api.mysite.ru/users');
Отмена запросов
CancelToken
в пакете Dio используется для отмены HTTP-запросов. CancelToken
позволяет отменить один или несколько запросов, которые были посланы с этим токеном. Это может быть очень полезно, например, когда пользователь выходит из приложения или переходит на другую страницу, где текущий сетевой запрос больше не нужен, что помогает избежать ненужного использования ресурсов и потенциальных ошибок состояния.
Пример настройки и использования CancelToken
:
1// Создаем CancelToken
2final cancelToken = CancelToken();
3
4// Отправляем запрос
5dio.get(
6 'https://api.mysite.ru/users',
7 cancelToken: token,
8).catchError((DioException e) {
9 if (CancelToken.isCancel(e)) {
10 print('Запрос отменен: ${error.message}');
11 } else {
12 print('При выполнении запроса возникла ошибка: ${e.message}');
13 }
14});
15
16// Отменяем запрос с сообщением "Запрос не актуален"
17token.cancel('Запрос не актуален');
Обработка ошибок
Dio предоставляет различные способы для обработки и классификации ошибок, которые могут возникнуть при выполнении HTTP-запросов. Это включает в себя ошибки соединения, тайм-ауты, ошибки HTTP-ответов и другие исключения. DioException
— это основной класс для всех ошибок, которые могут возникнуть при работе с Dio. Он имеет множество полезных свойств, которые помогают идентифицировать и корректно обработать ошибку.
Основные поля класса DioException
:
- type. Поле типа DioExceptionType, которое помогает определить категорию ошибки. Например, это может быть DioExceptionType.cancel, когда запрос был отменён, или DioExceptionType.response, когда сервер вернул ошибочный статус-код.
- message. Сообщение об ошибке.
- response. Объект Response, который содержит информацию о HTTP-ответе, если ошибка связана с полученным ответом. Это поле будет null, если ответ не был получен или ошибка произошла до того, как был получен ответ.
- requestOptions. Объект RequestOptions, который содержит детали о запросе, который привёл к ошибке.
- error. Оригинальная ошибка, связанная с данной ошибкой Dio.
Обработка ошибок с Dio обычно выполняется в блоке try-catch. Внутри catch
вы можете проверить, была ли ошибка связана с Dio, и использовать информацию из DioException
для принятия дальнейших действий или показа соответствующего сообщения пользователю.
Пример обработки ошибок:
1try {
2 final response = await dio.get('https://api.mysite.ru/users');
3 print(response.data);
4} on DioException catch (e) {
5 if (e.type == DioExceptionType.response) {
6 print('Запрос завершился с ошибкой: ${e.response?.statusCode}');
7 print('Данные ответа: ${e.response?.data}');
8 } else if (e.type == DioExceptionType.connectTimeout || e.type == DioExceptionType.receiveTimeout) {
9 print('Превышено время ожидания запроса');
10 } else if (e.type == DioExceptionType.cancel) {
11 print('Запрос был отменен');
12 } else {
13 print('При выполнении запроса возникла ошибка: ${e.message}');
14 }
15}
Плюсы и минусы
Dio — это отличный выбор для сложных приложений, требующих мощности и гибкости в работе с сетью, включая загрузку файлов, обработку ошибок, интерцепторы и многие другие типы сетевой активности.
Плюсы |
Минусы |
Мощность и гибкость. Поддерживает множество функций, таких как отмена запросов, повторные попытки запросов, тайм-ауты, кэширование и загрузка файлов. |
Избыточность. Dio может быть избыточным для простых приложений, что может привести к неиспользуемой сложности. |
Расширяемость. Возможность добавления кастомных интерцепторов и преобразователей. |
Отсутствие автоматической генерации кода. Dio требует ручного написания запросов, отсутствие автоматизации приводит к увеличению объёма кода. |
Активное сообщество. Имеет множество подробных документаций и примеров. |
Retrofit
Retrofit — это популярная библиотека в мире Android для упрощения HTTP-запросов и обработки ответов, которая была портирована и адаптирована для Dart/Flutter. Эта библиотека — обёртка над HTTP-клиентом Dio, которая предоставляет несколько дополнительных возможностей для работы с сетью, в частности удобный способ описания сетевых вызовов с помощью аннотаций.
Это позволяет разработчикам объявлять интерфейс взаимодействия с веб-сервисами в декларативном стиле, автоматизируя многие функции, обычно требующие ручной реализации.
Установка
Для начала работы с Retrofit необходимо добавить следующие зависимости в pubspec.yaml
:
1dependencies:
2 flutter:
3 sdk: flutter
4 retrofit: <latest_version>
5 json_annotation: <latest_version>
6
7dev_dependencies:
8 build_runner: <latest_version>
9 retrofit_generator: <latest_version>
10 json_serializable: <latest_version>
После добавления зависимости выполните команду flutter pub get
в терминале для установки пакетов.
Использование
Шаг 1: Создание модели данных
Перед началом взаимодействия необходимо создать объекты, которые мы будем использовать для отправки и получения данных. В качестве примера давайте создадим класс User
с полями name
и email
, используя пакет json_serializable
.
1import 'package:json_annotation/json_annotation.dart';
2
3part 'user.g.dart';
4
5@JsonSerializable()
6class User {
7 final String name;
8 final String email;
9 User({required this.name, required this.email});
10
11 factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
12 Map<String, dynamic> toJson() => _$UserToJson(this);
13}
После создания класса выполните команду flutter pub run build_runner build
для генерации кода JSON.
Шаг 2: Создание интерфейса API
Следующим шагом необходимо создать файл, где мы определим точки доступа к нашему API, используя аннотации пакета Retrofit. Давайте создадим класс ApiService
и реализуем методы getUser
и createUser
, которые будут отвечать за получение и создание пользователя.
1import 'package:retrofit/retrofit.dart';
2import 'package:dio/dio.dart';
3import 'user.dart';
4
5part 'api_service.g.dart';
6
7@RestApi(baseUrl: 'https://api.mysite.ru')
8abstract class ApiService {
9 factory ApiService(Dio dio, {String baseUrl}) = _ApiService;
10
11 /// Метод получения пользователя
12 @GET('/users/{id}')
13 Future<User> getUser(@Path('id') String id);
14
15 /// Метод создания пользователя
16 @POST('/users/create')
17 Future<User> createUser(@Body() User newUser);
18}
После создания класса выполните flutter pub run build_runner build
для генерации кода Retrofit.
Шаг 3: Выполнение запросов
Чтобы использовать получившийся ApiService
, создадим экземпляр Dio и передадим его в конструктор. После этого мы можем вызывать методы getUser
и createUser
для выполнения сетевого взаимодействия и обработки полученных данных в виде объекта класса User
.
1// Создание экземпляра Dio
2final dio = Dio();
3
4// Создание экземпляра ApiService
5final apiService = ApiService(dio);
6
7// Отправка запроса на получение пользователя
8final user = await apiService.getUser('123');
9print(user.name);
10
11// Отправка запроса на создание пользователя
12final newUser = User(name: 'Alex', email: 'alex@ya.ru');
13final createdUser = await apiService.createUser(newUser);
14print(createdUser.id);
Описание функционала
Запросы
Пакет Retrofit предоставляет набор аннотаций, с помощью которых можно легко описать сетевой интерфейс в виде интерфейса на Dart. Эти аннотации позволяют определить, как именно должны быть осуществлены HTTP-запросы, такие как GET, POST, DELETE, PUT и другие. Давайте рассмотрим два основных метода GET и POST вместе с их параметрами.
Метод GET
Используется для получения данных с сервера.
1@GET('/users/{id}')
2Future<User> getUser(@Path() String id);
Метод POST
Используется для отправки данных на сервер.
1@POST('/users/create')
2Future<User> createUser(@Body() User user);
Параметры запросов
@Path — используется для замены части URL путём передачи параметра метода.
1@GET('/users/{id}')
2Future<User> getUser(@Path('id') String id);
@Body — передаёт данные в тело запроса.
1@POST('/users/create')
2Future<User> createUser(@Body() User user);
@Query — используется для добавления параметров запроса.
1@GET('/users/query')
2Future<User> getUser(@Query('email') String email);
@Queries — позволяет добавлять мапу параметров запроса.
1@GET('/users/query')
2Future<User> getUser(@Queries() Map<String, dynamic> queries);
@Header — добавляет единичный заголовок к запросу.
1@GET('/users')
2Future<List<User>> getUsers(@Header('Authorization') String token);
@Headers — позволяет задать несколько фиксированных заголовков для метода.
1@GET('/users')
2@Headers({'Authorization': 'Bearer YOUR_ACCESS_TOKEN', 'Custom-Header': 'value'})
3Future<List<User>> getUsers();
Ответы
Класс HttpResponse
в пакете Retrofit представляет собой обёртку над ответом, полученным от HTTP-запроса через библиотеку Dio.
Основные поля класса HttpResponse
:
- statusCode. Код состояния HTTP-ответа (например, 200, 404 и т. д.). Статусный код позволяет легко определить, был запрос успешен или нет.
- statusMessage. Сообщение состояния ответа HTTP. Это описание кода состояния, предоставленное сервером.
- headers. Заголовки ответа; могут содержать полезную информацию, такую как cookies, тип контента, длина содержимого и другие метаданные.
- response. Экземпляр Response от Dio, позволяющий доступ ко всему сырому ответу, включая данные, заголовки, конфигурацию запроса и т. д.
Пример получения и использования HttpResponse
:
1@GET('/users/{id}')
2Future<HttpResponse<User>> getUser(@Path('id') String id);
1try {
2 final response = await apiService.getUser('123');
3 print('Код состояния: ${response.response.statusCode}');
4 print('Тело ответа: ${response.response.data}');
5
6 if (response.response.statusCode == 200) {
7 // Обработка успешного ответа
8 var data = response.response.data;
9 // Действия с данными
10 }
11} on DioException catch (e) {
12 if (e.response != null) {
13 // Это ошибка, при которой сервер вернул ответ с ошибкой
14 print('Код ошибки: ${e.response!.statusCode}');
15 print('Данные ошибки: ${e.response!.data}');
16 } else {
17 // Ошибка запроса или отмена запроса
18 print('При выполнении запроса возникла ошибка: ${e.message}');
19 }
20}
Плюсы и минусы
Retrofit — отличный выбор для средних и крупных проектов, где важна чистота кода и автоматизация, особенно если уже используется Dio.
Плюсы |
Минусы |
Автоматическая генерация кода. Retrofit использует аннотации для генерации кода API-клиента, что упрощает разработку. |
Ограниченная гибкость. Генерация кода может быть неудобной для сложных сценариев. |
Интеграция с Dio. Retrofit использует Dio под капотом, что даёт доступ ко всем его функциям. |
Зависимость от Dio. Retrofit использует Dio под капотом, что добавляет дополнительный уровень сложности и избыточности. |
Читаемость. Код становится более чистым и понятным благодаря аннотациям. |
Chopper
Chopper — это ещё одна библиотека для Flutter, которая предоставляет возможность создавать HTTP-запросы с использованием аннотаций подобно тому, как это делается в Retrofit, но с более нативной интеграцией с Dart. Это позволяет делать код более чистым и читабельным, а также упрощает процесс разработки сетевых запросов.
Установка
Для начала работы с Chopper необходимо добавить следующие зависимости в pubspec.yaml
:
1dependencies:
2 flutter:
3 sdk: flutter
4 chopper: <latest_version>
5
6dev_dependencies:
7 build_runner: <latest_version>
8 chopper_generator: <latest_version>
После добавления зависимости выполните команду flutter pub get
в терминале для установки пакетов.
Использование
Шаг 1: Создание модели данных
Перед началом взаимодействия необходимо создать объекты, которые мы будем использовать для отправки и получения данных. В качестве примера давайте снова создадим класс User
с полями name
и email
, используя пакет json_serializable
.
1import 'package:json_annotation/json_annotation.dart';
2
3part 'user.g.dart';
4
5@JsonSerializable()
6class User {
7 final String name;
8 final String email;
9 User({required this.name, required this.email});
10
11 factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
12 Map<String, dynamic> toJson() => _$UserToJson(this);
13}
После создания класса выполните команду flutter pub run build_runner build
для генерации кода JSON.
Шаг 2: Создание конвертера данных
Чтобы использовать json_serializable
модели в Chopper, нам необходимо написать кастомный конвертер. Для этого наследуемся от класса JsonConverter
и переопределяем метод convertResponse
, как показано ниже.
1class JsonSerializableConverter extends JsonConverter {
2 @override
3 Response<ResultType> convertResponse<ResultType, Item>(Response response) {
4 final Response<dynamic> jsonResponse = super.convertResponse(response);
5 final body = jsonResponse.body;
6 return jsonResponse.copyWith<ResultType>(
7 body: _fromJson<ResultType, Item>(body),
8 );
9 }
10
11 dynamic _fromJson<ResultType, Item>(dynamic element) {
12 if (element is Iterable) {
13 return element.map((item) => _fromJson<Item, void>(item)).toList();
14 } else if (ResultType == User && element is Map<String, dynamic>) {
15 return User.fromJson(element) as ResultType;
16 }
17 return element;
18 }
19}
Шаг 3: Создание интерфейса API
Следующим шагом необходимо создать файл, где мы определим точки доступа к нашему API, используя аннотации пакета Chopper. Давайте создадим класс ApiService
и реализуем методы getUser
и createUser
, которые будут отвечать за получение и создание пользователя.
1import 'package:chopper/chopper.dart';
2
3part 'api_service.chopper.dart';
4
5@ChopperApi(baseUrl: '/users')
6abstract class ApiService extends ChopperService {
7 static ApiService create([ChopperClient? client]) => _$ApiService(client);
8
9 @Get(path: '/{id}')
10 Future<Response<User>> getUser(@Path() String id);
11
12 @Post(path: '/create')
13 Future<Response<User>> createUser(@Body() User user);
14}
После создания класса выполните flutter pub run build_runner build
для генерации кода Chopper.
Шаг 4: Выполнение запросов
Чтобы использовать получившийся ApiService
, создадим экземпляр ChopperClient
и вызовем получение нашего сервиса с помощью getService<ApiService>()
. После этого мы можем вызывать методы getUser
и createUser
для выполнения сетевого взаимодействия и обработки полученных данных в виде объекта класса User
.
1// Создание экземпляра ChopperClient
2final chopperClient = ChopperClient(
3 baseUrl: 'https://api.mysite.ru',
4 services: [ApiService.create()],
5 converter: JsonSerializableConverter(),
6);
7
8// Получение экземпляра ApiService
9final apiService = chopperClient.getService<ApiService>();
10
11// Отправка запроса на получение пользователя
12final response = await apiService.getUser('123');
13print(response.body.name);
14
15// Отправка запроса на создание пользователя
16final newUser = User(name: 'Alex', email: 'alex@ya.ru');
17final response = await apiService.createUser(newUser);
18print(response.body.id);
Описание функционала
Запросы
Chopper представляет собой библиотеку, которая облегчает работу с HTTP-запросами, такими как GET, POST, DELETE, PUT и другими, предоставляя возможность организации и чёткого описания сетевых запросов через аннотации и генерацию кода. Давайте рассмотрим два основных метода GET и POST вместе с их параметрами.
Метод GET
Используется для получения данных с сервера.
1@Get(path: '/users/{id}')
2Future<Response<User>> getUser(@Path() String id);
Метод POST
Используется для отправки данных на сервер.
1@Post(path: '/users/create')
2Future<Response<User>> createUser(@Body() User user);
Параметры запросов
@Path — используется для замены части URL путём передачи параметра метода.
1@Get(path: '/users/{id}')
2Future<Response<User>> getUser(@Path('id') String id);
@Body — отправляет данные в теле запроса.
1@Post(path: '/users/create')
2Future<Response<User>> createUser(@Body() User user);
@Query — добавляет параметры запроса к URL.
1@Get(path: '/users/query')
2Future<Response<User>> getUser(@Query('email') String email);
@ QueryMap — позволяет добавлять мапу параметров запроса.
1@Get(path: '/users/query')
2Future<Response<User>> getUser(@QueryMap() Map<String, dynamic> queries);
@Header — используется для добавления одиночного заголовка к запросу.
1@Get(path: '/users')
2Future<Response<List<User>>> getUsers(@Header('Authorization') String token);
@Headers — позволяет задать несколько заголовков сразу.
1@Get(path: '/users')
2@Headers({'Authorization': 'Bearer YOUR_ACCESS_TOKEN', 'Custom-Header': 'value'})
3Future<Response<List<User>>> getUsers();
Ответы
Класс Response
— это основной механизм для представления и обработки HTTP-ответов. Response
обеспечивает доступ не только к телу ответа, но и ко всем связанным с ним метаданным, включая статус-код, заголовки и другую важную информацию, что позволяет эффективно управлять результатами сетевых запросов.
Основные поля класса Response
:
- body. Содержимое ответа. Chopper позволяет автоматически конвертировать это содержимое из JSON в объект при помощи конвертера.
- statusCode. Код состояния HTTP-ответа (например, 200, 404 и т. д.). Статусный код позволяет легко определить, был запрос успешен или нет.
- headers. Заголовки ответа; могут содержать полезную информацию, такую как cookies, тип контента, длина содержимого и другие метаданные.
- isSuccessful. Завершился ли запрос успешно. Возвращает true, если статусный код находится в диапазоне 200–299.
- base. Полный HTTP-ответ, полученный от библиотеки http, которая используется внутри Chopper.
- error. Содержимое ответа, когда запрос завершается ошибкой.
Пример получения и использования Response
:
1@Get(path: '/users/{id}')
2Future<Response<User>> getUser(@Path('id') String id);
1final response = await apiService.getUser('123');
2if (response.isSuccessful) {
3 print('Код состояния: ${response.statusCode}');
4 print('Тело ответа: ${response.data}');
5} else {
6 print('Код ошибки: ${response.statusCode}');
7 print('Данные ошибки: ${response.error}');
8}
Дополнительные возможности
Интерцепторы
Интерцепторы в Chopper предоставляют гибкий способ взаимодействия с API, позволяя адаптировать, модифицировать и предварительно обрабатывать данные на любом этапе передачи данных. Интерцепторы могут быть созданы как функции, которые принимают и возвращают модифицированный Request
и Response
, а также в виде классов, реализующих интерфейс Interceptor
.
Пример настройки и использования интерцепторов:
1void main() async {
2 final chopperClient = ChopperClient(
3 baseUrl: 'https://api.mysite.ru',
4 services: [ApiService.create()],
5 converter: JsonConverter(),
6 interceptors: [
7 _addToken,
8 _checkLoggedIn,
9 CustomInterceptor(),
10 ],
11 );
12
13 final apiService = chopperClient.getService<ApiService>();
14 final response = await apiService.getUsers();
15}
16
17// Добавляет заголовок Authorization ко всем исходящим запросам.
18RequestInterceptor _addToken(Request request) {
19 return applyHeaders(
20 request,
21 {'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
22 override: false,
23 );
24}
25
26// Проверяет, не вернулся ли статус 401 Unauthorized
27ResponseInterceptor _checkLoggedIn(Response response) {
28 if (response.statusCode == 401) {
29 print('Необходима авторизация');
30 }
31 return response;
32}
33
34class CustomInterceptor implements RequestInterceptor, ResponseInterceptor {
35 @override
36 Future<Request> onRequest(Request request) async {
37 // Здесь можно внести изменение в запрос
38 return request;
39 }
40
41 @override
42 Future<Response> onResponse(Response response) async {
43 // Здесь можно внести изменение в ответ
44 return response;
45 }
46}
Конвертеры
Chopper позволяет легко интегрировать конвертеры данных, чтобы автоматически преобразовывать JSON в объекты Dart и наоборот.
Встроенные конвертеры
Chopper предоставляет встроенный JsonConverter
, который покрывает большинство стандартных случаев использования.
1final chopperClient = ChopperClient(
2 baseUrl: 'https://api.mysite.ru',
3 services: [ApiService.create()],
4 converter: JsonConverter(),
5);
Создание пользовательских конвертеров
Вы можете создать свой собственный конвертер, если встроенные варианты не отвечают вашим требованиям. Например, если API возвращает данные в специфическом формате или если вы хотите использовать более сложные объекты Dart, которые требуют особой логики сериализации/десериализации. Для создания своего конвертера необходимо определить класс, который реализует интерфейс Converter
или наследует от JsonConverter
.
Пример пользовательского конвертера:
1void main() async {
2 final chopperClient = ChopperClient(
3 baseUrl: 'https://api.mysite.ru',
4 services: [ApiService.create()],
5 converter: MyCustomConverter(),
6 );
7
8 final apiService = chopperClient.getService<ApiService>();
9 final response = await apiService.getUsers();
10}
11
12class MyCustomConverter extends Converter {
13 @override
14 Request convertRequest(Request request) {
15 // Логика преобразования данных запроса
16 return super.convertRequest(request);
17 }
18
19 @override
20 Response<BodyType> convertResponse<BodyType, SingleItemType>(
21 Response response,
22 TypeConverter converter,
23 ) {
24 // Логика преобразования данных ответа
25 return response.replacement<BodyType>(
26 body: YourCustomDeserializeFunction(response.body),
27 );
28 }
29}
Обработка ошибок
Обработка ошибок критически важна при разработке приложений, это обеспечивает устойчивость и надёжность приложения при взаимодействии с внешними API. В пакете Chopper предусмотрены различные механизмы для эффективной обработки ошибок, возникающих во время HTTP-запросов.
Перехват исключений на уровне интерцепторов
Первый уровень перехвата и обработки ошибок можно выполнить при помощи интерцепторов. Благодаря интерцепторам вы можете анализировать HTTP-ответы, прежде чем они будут обработаны сервисом или вашим приложением.
1void main() async {
2 final chopperClient = ChopperClient(
3 baseUrl: 'https://api.mysite.ru',
4 services: [ApiService.create()],
5 converter: JsonConverter(),
6 interceptors: [handleErrorInterceptor],
7 );
8
9 final apiService = chopperClient.getService<ApiService>();
10 final response = await apiService.getUsers();
11}
12
13Response handleErrorInterceptor(Response response) {
14 if (!response.isSuccessful) {
15 switch (response.statusCode) {
16 case 404:
17 throw ResourceNotFoundException('Resource Not Found');
18 case 500:
19 throw ServerErrorException('Internal Server Error');
20 default:
21 throw UnknownErrorException('Unknown Error Occurred');
22 }
23 }
24 return response;
25}
Обработка исключений при вызове сервиса
Когда вы делаете вызовы к API через сервисы Chopper, используйте блоки try-catch
для перехвата и обработки исключений.
1void main() async {
2 final chopperClient = ChopperClient(
3 baseUrl: 'https://api.mysite.ru',
4 services: [ApiService.create()],
5 converter: JsonConverter(),
6 interceptors: [handleErrorInterceptor],
7 );
8
9 try {
10 final apiService = chopperClient.getService<ApiService>();
11 final response = await apiService.getUsers();
12 if (response.isSuccessful && response.body != null) {
13 // Обработка полученных данных
14 } else {
15 // Обработка случая, когда тело ответа пусто или некорректно
16 print('Ошибка: пустой или некорректный ответ');
17 }
18 } catch (e) {
19 // Отлавливаем исключения
20 print('Unexpected error: $e');
21 }
22}
Плюсы и минусы
Chopper — это отличный вариант для небольших и средних проектов, которые хотят использовать генерацию кода и иметь простую архитектуру API-запросов, без избыточных функций.
Плюсы |
Минусы |
Простота. Хорош для небольших и средних проектов, где не требуется значительная сложность. |
Меньше возможностей. Chopper не предоставляет столь широкий функционал, как Dio, особенно в области гибкости запросов. |
Автоматическая генерация кода. Упрощает создание API-клиентов с помощью генерации кода для выполнения запросов. |
Ограниченная гибкость. Генерация кода может быть неудобной для сложных сценариев. |
Читаемость. Код становится более чистым и понятным благодаря аннотациям. |
Меньшая популярность. Имеет меньше примеров и документаций по сравнению с Dio и Retrofit. |
Что в итоге
Выбор подходящего пакета для работы с сетью во Flutter может значительно повлиять на производительность приложения, упростить процесс разработки и повысить надёжность сетевых запросов.
Каждый из инструментов, которые мы разобрали в этом параграфе, предлагает различные возможности для эффективной работы с HTTP-запросами, такие как удобное создание запросов, поддержка интерцепторов и автоматическая сериализация данных.
Напоследок — пара фраз о том, в каких ситуациях и кому подойдёт тот или иной инструмент:
-
Dio лучше всего подходит для разработчиков, которым нужен высокий уровень контроля и настройки запросов без встроенного механизма аннотаций.
-
Retrofit служит хорошим выбором для тех, кто предпочитает строгую и организованную структуру для API-взаимодействий и готов инвестировать время в начальную настройку для упрощения последующей работы.
-
А Chopper подходит для тех, кто ищет средний путь между гибкостью Dio и организованностью Retrofit, предлагая при этом механизм генерации кода и встроенные интерцепторы.