Работа с Retrofit | Evgenii Baburin
Главная страница Работа с Retrofit
Публикация
Отменить

Работа с Retrofit

Retrofit - это библиотека для работы с REST API в Android. Она позволяет легко взаимодействовать с сервером и выполнять HTTP-запросы. Библиотека сама создаёт необходимые классы для выполнения запросов и обрабатывает ответы. Retrofit поддерживает множество функций, среди которых аутентификация и авторизация, поддержка JSON и XML, поддержка многопоточности и многое другое. Основана на библиотеке OkHttp, работает на Java 8 и выше, Android API 21 (Android 5) и выше.

Общие сведения

Описание

Для работы с Retrofit необходимы три вещи:

  1. класс, который используется как модель получаемого ответа (POJO, можно сгенерировать плагином JSON to Kotlin Class или на сайте jsonschema2pojo);
  2. интерфейс, который определяет возможные запросы. Каждая его функция представляет из себя один из возможных вызовов API. Всегда возвращает Call-объект с типом ожидаемого результата (POJO). Если тип ответа не интересует - можно указать Call<Responce>. Перед функцией указывается аннотация с типом запроса (@GET, @POST и так далее) и параметром (адресом метода API). В параметрах самой функции перечисляются параметры запроса. Динамические параметры (которые могут изменяться в процессе работы приложения) следует указывать в фигурных скобках, например /users/{user}/repositories;
  3. экземпляр класса Retrofit.Builder(), который содержит базовый адрес для API, информацию о выбранном конвертере и так далее. Далее в его методе create() передаётся класс интерфейса с запросами к сервису.

Экземпляры вызовов Call могут выполняться как синхронно, так и асинхронно. Каждый экземпляр может быть использован только один раз, но вызов функции clone() создаст новый экземпляр, который можно использовать. Для синхронного запроса используется метод Call.execute(), для асинхронного - метод Call.enqueue().

В результате Retrofit сделает запрос, получит ответ и обработает его. Остаётся только вызывать нужные функции класса-модели для извлечения данных. Основная часть работы происходит в функции onResponse() объекта Callback, ошибки выводятся в onFailure() (неправильный адрес сервера, некорректные формат данных, неправильный формат класса-модели и так далее). Класс Response имеет функцию isSuccessful() для успешной обработки запроса (двухсотые коды). В ошибочных ситуациях можно обработать ошибку в функции errorBody() класса ResponseBody.

Подключение Retrofit в проекте

Подключение в Gradle:

1
2
implementation 'com.squareup.retrofit2:retrofit:2.9.0' // Retrofit
implementation 'com.squareup.retrofit2:converter-gson:2.9.0' // конвертер Gson

Подключение в Maven:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
  <groupId>com.squareup.retrofit2</groupId>
  <artifactId>retrofit</artifactId>
  <version>2.9.0</version>
</dependency>

<dependency>
  <groupId>com.squareup.retrofit2</groupId>
  <artifactId>converter-gson</artifactId>
  <version>2.9.0</version>
</dependency>

Пример простой реализации

В простейшей форме интерфейс выглядит следующим образом (на примере Яндекс.Погоды):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// интерфейс, определяющий функции для запросов
interface WeatherService {
    @GET("v2/informers")
    fun getWeather(
        @Header("X-Yandex-API-Key") auth: String = "YOUR_API_KEY",
        @Query("lat") lat: Double,
        @Query("lon") lon: Double,
        @Query("lang") lang: String = "ru_RU"
    ): Call<Weather>
}

// модель ответа; класс pojo
data class Weather(
    val fact: Fact,
    val forecast: Forecast,
    val info: Info,
    val now: Int,
    val now_dt: String
)

fun main() {
    // обратный вызов (колбэк) для обработки ответа
    val callback = object: Callback<Weather> {
        override fun onResponse(p0: Call<Weather>, p1: Response<Weather>) {
            // в случае получения ответа
        }

        override fun onFailure(p0: Call<Weather>, p1: Throwable) { 
            // в случае получения ошибки
        }
    }

    // создаём объект Retrofit для отправки запроса
    val retrofit = Retrofit.Builder()
        .baseUrl("https://api.weather.yandex.ru/")
        .addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))
        .build()
        .create(WeatherService::class.java)

    // создание объекта WeatherService для отправки запросов
    val weatherService: WeatherService = retrofit

    // отправка асинхронного запроса
    weatherService
        .getWeather(lat = 55.62, lon = 37.62)
        .enqueue(callback) // асинхронный способ

    // отправка синхронного запроса
    val request = weatherService.getWeather(lat = 55.62, lon = 37.62).execute().body()
    val weather = request.body()
}

Подробности

Функции

Интерфейс Call<T>

Call<T> - объект, возвращаемый в результате отправки асинхронного запроса, где T - тип данных, который ожидается получить. Полезные функции, наследуемые от интерфейса Call:

  • cancel() - отменить выполняющийся запрос. Если запрос ещё не был начат, то после вызова функции cancel() он будет отменен; если запрос уже был завершен - то ничего не произойдет. При этом выполнение cancel() не гарантирует, что запрос не будет выполнен на сервере - вызов cancel() лишь прерывает процесс ожидания ответа от сервера на клиентской стороне;
  • clone() - создаёт новый, идентичный вызываемому экземпляр Call<T> для повторного использования;
  • enqueue(callback: Callback<T>) - асинхронная функция выполнения запроса на сервер и получения ответа в виде объекта T через колбэки Callback<T>;
  • execute() - функция синхронного выполнение запроса на сервер. Может блокировать основной поток (или тот поток, в котором вызван);
  • isCancelled() - возвращает true, если запрос прерывался функцией cancel();
  • isExecuted() - возвращает true, если запрос вызывался функцией execute() или enqueue();
  • request() - возвращает экземпляр okhttp3.Request - оригинальный запрос.

Интерфейс Callback<T>

Интерфейс используется для обработки асинхронных ответов от сервера. Он предоставляет методы обратного вызова, один из которых вызывается, когда запрос выполнен и получен ответ (или произошла ошибка). Содержит следующие методы:

  • onFailure(call: Call<T>, t: Throwable) - вызывается при возникновении сетевого исключения при обращении к серверу или при возникновении непредвиденного исключения при создании запроса или обработке ответа;
  • onResponse(call: Call<T>, responce: Response<T>) - вызывается в случае успешного HTTP-запроса для его обработки.

Класс Response<T>

Представляет из себя обертку для ответа от сервера на запрос. Содержит в себе информацию о коде состояния HTTP, заголовках ответа и тело ответа (которое может быть представлено в экземпляре объекта T). Содержит следующие методы:

  • body() - возвращает тело ответа, представленное в объект T, если запрос успешен, иначе - null;
  • code() - возвращает код состояния HTTP, который указывает на выполнение запроса или ошибку;
  • error() - создаёт синтетический объект ошибки;
  • errorBody() - метод возвращает ResponseBody, который представляет собой тело ответа с сервера в случае неуспешного выполнения запроса. Может содержать дополнительную информацию об ошибке;
  • headers() - возвращает объект Headers, который представляет собой заголовки ответа от сервера;
  • isSuccessful() - возвращает true, если code() возвращает [200..300), иначе - false;
  • message() - возвращает сообщение статуса HTTP (или null);
  • raw() - необработанный ответ от сервера;
  • success() - создаёт синтетический объект успешного запроса.

Классы Retrofit и Retrofit.Builder

Retrofit - базовый класс для создания запросов. Содержит класс Retrofit.Builder() для создания нового экземпляра Retrofit и следующие методы:

  • baseUrl() - указывает основной адрес (URL), к которому будут добавляться относительные пути запросов;
  • callAdapter() - возвращает CallAdapter из доступных фабрик;
  • callAdapterFactories() - используется для получения списка фабрик адаптеров вызовов (Call Adapter Factories), которые определяют, как Retrofit должен обрабатывать результаты HTTP-запросов и преобразовывать их в объекты Java;
  • callbackExecutor() - позволяет задать исполнителя для обработки колбэков при получении ответов на запросы;
  • callFactory() - позволяет установить фабрику вызовов (Call Factory), которая определяет, каким образом будут создаваться экземпляры Call<T> для каждого запроса к API;
  • converterFactories() - используется для получения списка фабрик конвертеров (Converter Factories), которые определяют, как Retrofit будет преобразовывать данные запросов и ответов между байтами и Java-объектами;
  • create() - используется для создания экземпляра интерфейса, который определяет точки входа для сетевых запросов. Он вызывается на объекте типа Retrofit и требует передачи класса интерфейса, описывающего эндпоинты и параметры запросов. Затем Retrofit создаст и вернет реализацию этого интерфейса, которую можно использовать для выполнения сетевых запросов;
  • nextCallAdapter() - используется для добавления пользовательских адаптеров вызовов (Call Adapter) для обработки результатов HTTP-запросов. Retrofit позволяет зарегистрировать несколько адаптеров вызовов, которые позволяют обрабатывать результаты запросов с использованием различных подходов, таких как использование библиотек реактивного программирования (например, RxJava или Kotlin Coroutines), использование собственных типов оберток или добавление пользовательской логики обработки. Метод nextCallAdapter() позволяет добавить следующий адаптер вызовов к цепочке адаптеров. При выполнении запроса Retrofit будет пытаться использовать адаптеры вызовов в порядке их добавления, пока не будет найден подходящий адаптер для обработки результата вызова метода API;
  • nextRequestBodyConverter() - позволяет добавить следующий конвертер тела запроса к цепочке конвертеров
  • requestBodyConverter() - используется для добавления пользовательского конвертера запросов (Request Body Converter) для преобразования объектов Java в тела запросов HTTP;
  • stringConverter() - используется для добавления конвертера ответов (Response Converter) для преобразования тел ответов HTTP в строки.

Функции класса Retrofit.Builder:

  • addCallAdapterFactory() - добавляет фабрику адаптеров вызовов;
  • addConverterFactory() - добавляет фабрику конвертера вызова;
  • baseUrl() - указывает базовый URL API;
  • build() - завершает процесс настройки и создает экземпляр Retrofit, который можно использовать для выполнения сетевых запросов;
  • callAdapterFactories() - используется для получения списка фабрик адаптеров вызовов (Call Adapter Factories), которые определяют, как Retrofit должен обрабатывать результаты HTTP-запросов и преобразовывать их в объекты Java;
  • callbackExecutor() - позволяет задать исполнителя для обработки колбэков при получении ответов на запросы (по умолчанию Retrofit использует исполнителя, связанного с UI-потоком Android);
  • callFactory() - позволяет установить фабрику вызовов (Call Factory), которая определяет, каким образом будут создаваться экземпляры Call<T> для каждого запроса к API;
  • client() - используется для установки клиента HTTP, который будет использоваться для выполнения запросов к серверу (по умолчанию - okhttp3.OkHttpClient);
  • converterFactories() - используется для получения списка фабрик конвертеров (Converter Factories), которые определяют, как Retrofit будет преобразовывать данные запросов и ответов между байтами и Java-объектами;
  • validateEagerly() - используется для задания режима предварительной проверки интерфейса API на корректность во время построения экземпляра Retrofit. При создании экземпляра Retrofit с помощью Retrofit.Builder, Retrofit проходит через все методы в интерфейсе API и выполняет предварительные проверки для обнаружения потенциальных проблем, таких как отсутствие аннотаций, неправильные типы параметров, неправильные типы возвращаемых значений и так далее.

Аннотации

Каждая функция должна иметь аннотацию, которая представляет тип метода API и URL. Существует восемь встроенных аннотаций:

  • @DELETE
  • @GET
  • @HEAD
  • @OPTIONS
  • @PATCH
  • @POST
  • @PUT
  • @HTTP (для нестандартных методов API)
1
2
3
4
5
@GET("users/list")
fun getUsersList(): Call<List<Users>>

@GET("users/list?sort=desc")
fun getSortedUsersList(): Call<List<Users>>

@Path, @Query и @QueryMap

URL-адрес в параметре может быть обновлён динамически с использованием блоков замены и параметров метода API. Блок замены представляет собой буквенно-цифровую строку, окруженную символами { и }. Соответствующий параметр должен быть помечен @Path с использованием той же строки:

1
2
@Get("group/{id}/users")
fun getGroupList(@Path("id") groupId: Int): Call<List<Users>>

Для более сложных запросов можно использовать Map:

1
2
@GET("group/{id}/users")
fun getGroupList(@Path("id") groupId: Int, @QueryMap options: Map<String, String): Call<List<Users>>

Аннотация @Query используется при запросах с параметрами (одна аннотация указывает один параметр). В одном запросе можно указывать несколько @Query-параметров.

1
2
3
4
5
6
7
8
9
10
@GET("products/cats?category=5&sort=desc") // пример c параметрами в аннотации
fun getAllUsers(): Call<Users>

@GET("products/users?sort=desc") // пример с параметрами в функции
fun getAllUsers(@Query("department") departmentId: Int): Call<Users>
// получится запрос вида http://api.example.com/products/users?sort=desc&department=5.

@GET("/friends")
fun friends(@Query("group") vararg groupd: String): Call<ResponseBody>
// при вызове friends("coworker", "bowling") создаст запрос типа /friends?group=coworker&group=bowling

QueryMap можно использовать, передав пары ключ-значение с параметрами:

1
2
@GET("api/data")
fun getata(@QueryMap(encoded = false) parameters: Map<String, String>): Call<DataResponse>

Параметр encoded сообщает Retrofit, нужно ли кодировать данные (true) или они уже поступают закодированными (false). Например:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@GET("api/data")
fun getata(@QueryMap(encoded = false) parameters: Map<String, String>): Call<DataResponse>
// ...
parameters.put("param", "some value with spaces")
parameters.put("key", "special!characters")
apiService.getData(parameters)
// https://example.com/api/data?param=some value with spaces&key=special!characters

@GET("api/data")
fun getata(@QueryMap(encoded = true) parameters: Map<String, String>): Call<DataResponse>
// ...
parameters.put("param", "some value with spaces")
parameters.put("key", "special!characters")
apiService.getData(parameters)
// https://example.com/api/data?param=some%20value%20with%20spaces&key=special%21characters

@QueryName

Указывает на параметр запроса, которые добавлен к URL, но не имеет значения. Параметр encoded сообщает Retrofit, нужно ли кодировать данные (true) или они уже поступают закодированными (false):

1
2
3
4
5
6
7
8
@GET("/friends")
fun friends(@QueryName filter: String): Call<ResponseBody>
// при вызове foo.friends("contains(Bob)") составит запрос /friends?contains(Bob)

 @GET("/friends")
 fun friends(@QueryName(encoded=true) filter: String)
 // при вызове foo.friends("name+age") составит запрос /friends?name+age
 Call<ResponseBody> friends(@QueryName(encoded=true) String filter);

@HTTP

Для нестандартных HTTP-методов (которых нет в стандартной реализации Retrofit) существует аннотация HTTP:

1
2
@HTTP(method = "CUSTOM_METHOD", path = "/api/create/", hasBody = true)
fun createEntity(@Body request: MyRequest): Call<MyResponse>

hasBody здесь используется для указания того, что запрос содержит тело (body) данных (например, в формате JSON). Если параметр hasBody установлен в true, то запрос считается POST-запросом, иначе - GET-запросом.

Также это аннотация можно использовать для DELETE с объектом в запросе:

1
2
@HTTP(method = "DELETE", path = "remove/", hasBody = true)
fun deleteObject(@Body objecty: RequestBody): Call<ResponseBody>

@Body

Объект может быть указан для использования в качестве тела HTTP-запроса (@PUT и @POST) с аннотацией @Body - такой объект также должен быть преобразован с помощью конвертера, указанного в экземпляре Retrofit. Если конвертер не добавлен, можно использовать только RequestBody:

1
2
@POST("users/new")
fun createUser(@Body user: User): Call<User>

@Header и @HeaderMap

Аннотации позволяют установить заголовки запроса (HTTP Headers). Заголовки с одинаковыми именами не перезаписывают друг друга, а дополняются.

1
2
3
4
5
@GET("/")
fun foo(@Header("Accept-Language") lang: String): Call<ResponseBody>

@GET("/search")
fun list(@HeaderMap headers: Map<String, String>)

@Headers

Позволяет указать HTTP-заголовки вместе:

1
2
@Headers({"Cache-Control: max-age=640000", "User-Agent: MyAppName"})
@GET("users/{username}")

@Multipart, @Part и @PartMap

Используется при загрузке файлов:

1
2
3
@Multipart
@POST("users/{username}")
fun uploadImage(@Part("description") description: String, @Part("image") image: RequestBody): Call<Response>

@FormUrlEncoded, Field и FieldMap

Означает, что в теле запроса будет использоваться кодировка URL-адреса формы. Пары ключ-значение должны быть объявлены как параметры и помечены символом @Field. Запросы, сделанные с помощью этой аннотации, будут иметь MIME-тип application/x-www-form-urlencoded. Имена и значения полей будут закодированы в UTF-8, прежде чем будут закодированы в URI в соответствии с RFC-3986:

1
2
3
@FormUrlEncoded
@POST("/foo/bar")
fun someFun(@FieldMap users: Map<String, String>): Call<SomeResponse>

@Url

URL-адрес будет сопоставлен с базовым адресом (baseUrl()):

1
2
@GET
fun getZipFile(@Url url: String): Call<File>

@SkipCallbackExecutor

Представляет собой интерфейс-маркер, который используется для пропуска автоматического выполнения колбэков (Callback) на заданном экземпляре Call<T>. Это дает возможность самостоятельно управлять выполнением колбэков и выполнять их вручную вместо того, чтобы делегировать автоматическое выполнение библиотеке Retrofit.Чтобы использовать SkipCallbackExecutor, необходимо создать свою собственную реализацию этого интерфейса и использовать ее в определенных методах интерфейса API.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// создание интерфейса-маркера
interface SkipCallbackExecutor { }

// создание интерфейса API
@GET("users")
fun getUsers(skip: SkipCallbackExecutor): Call<List<User>>

// выполнение запроса
val call = apiService.getUsers(object: SkipCallbackExecutor {})
val response = call.execute()
if (response.isSuccessful) {
    // обработка полученных данных
} else {
    // обработка ошибки
}

@Streaming

Относится к поддержке потоковой передачи данных между клиентом и сервером. Это означает, что данные могут быть переданы и обработаны частями, по мере их получения или отправки, вместо того чтобы ожидать полного завершения передачи данных, как это происходит в синхронной или блокирующей передаче данных. @Streaming часто используется для передачи больших объемов данных или файлов, когда нецелесообразно или невозможно ожидать завершения полной передачи данных перед обработкой. Также это может быть полезно для снижения использования памяти при работе с большими объемами данных. Retrofit поддерживает два типа streaming:

  • потоковая передача запроса (Request Streaming): в этом режиме клиент посылает запрос на сервер по частям. Например, это может быть полезно при загрузке больших файлов на сервер;
  • потоковая передача ответа (Response Streaming): в этом режиме сервер отправляет ответ клиенту по частям. Например, это может быть полезно при загрузке больших файлов с сервера или при получении данных в реальном времени.
1
2
3
4
5
6
7
@Streaming
@POST("upload")
fun uploadFile(@Body fileStream: RequestBody): Call<ResponseBody> // потоковая передача запроса

@Streaming
@GET("download")
fun downloadFile(): Call<ResponseBody> // потоковая передача ответа

При использовании @Streaming следует использовать асинхронные запросы.

@Tag

Используется для присваивания тегов (идентификаторов) запросам, что позволяет более удобно управлять ими, особенно в случаях, когда одновременно выполняется несколько запросов. Одна из основных целей аннотации @Tag - это обеспечить возможность отмены или приостановки выполнения группы запросов, помеченных тегом.

1
2
3
4
5
6
7
8
9
10
11
12
@GET("users")
@Tag("users_request")
fun getUsers(): Call<List<User>>

@POST("posts")
@Tag("posts_request")
fun createPost(@Body post: Post): Call<Post>

// ...

val callUsers = apiService.getUsers();
callUsers.cancel() // остановит все функции, где указан такой же тег, как у getUsers()

Конвертеры

По умолчанию Retrofit может десериализовать тела HTTP только в тип ResponseBody библиотеки OkHttp и может принимать только свой тип RequestBody для @Body. Конвертеры добавляются для поддержки других типов - он преобразует полученный ответ с сервера в объект. Можно выбрать несколько конвертеров (при этом порядок важен) или создать собственный (реализовав интерфейс на основе абстрактного класса Converter.Factory). Существующие конвертеры:

  • JSON:
    • Gson com.squareup.retrofit2:converter-gson;
    • Jackson com.squareup.retrofit2:converter-jackson;
    • Moshi com.squareup.retrofit2:converter-moshi;
  • Protocol Buffers:
    • Protobuf com.squareup.retrofit2:converter-protobuf;
    • Wire com.squareup.retrofit2:converter-wire;
  • XML
    • Simple XML: com.squareup.retrofit2:converter-simplexml
    • JAXB: com.squareup.retrofit2:converter-jaxb
  • Примитивные типы данных
    • Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars

Конвертер не включен в состав Retrofit и его необходимо импортировать в проект отдельно.

Адаптеры

Retrofit может быть расширен адаптерами для взаимодействия с другими библиотеками, например RxJava, Guava и другими. Чтобы добавить адаптер, необходимо использовать функцию retrofit2.Retrofit.Builder.addCallAdapterFactory(Factory). При использовании адаптера, интерфейсы Retrofit могут возвращать типы других библиотек, например, Observable, Flowable, Single из RxJava и другие.

1
2
3
4
5
6
7
val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    .build()

@GET("users")
fun getUsers(): Observable<List<User>>

Перехватчики

В Retrofit для обработки сетевых запросов и ответов предоставляется возможность использовать перехватчики. Они позволяют вмешиваться в процесс выполнения запросов и обработки ответов, что даёт контроль над данными до и после их отправки и получения с сервера.

Основное применение - это добавление или модификация заголовков запросов и ответов, логирование, кэширование, подмена данных и другое. В Retrofit перехватчики реализованы с помощью интерфейса Interceptor, который состоит из одной функции - intercept(chain: Chain). В этом методе можно выполнять действия до выполнения запроса (chain.proceed(request)) или после получения ответа (chain.proceed(response)), а также изменять запросы и ответы.

Перехватчики добавляются к экземпляру OkHttpClient, который Retrofit использует для выполнения сетевых запросов.

В примере перехватчик добавляет заголовок Authorization:

1
2
3
4
5
6
7
8
9
10
11
12
13
val httpClient = OkHttpClient.Builder()
    .addInterceptor { chain ->
        val originalRequest = chain.request()
        val modofiedRequest = originalRequest.newBuilder()
            .header("Authrization", "Bearer TOKEN")
            .build()
        chain.proceed(modifiedRequest)
    }.build()

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .client(httpClient)
    .build()

Исключения

HttpException может вызываться неожиданным (не двухсотым) ответом сервера. Содержит функции:

  • code() - целое число; код HTTP;
  • message() - строка; сообщение об ошибке;
  • response() - полный ответ.
Публикация защищена лицензией CC BY 4.0 .
Популярные теги
Содержание
Популярные теги