Retrofit - это библиотека для работы с REST API в Android. Она позволяет легко взаимодействовать с сервером и выполнять HTTP-запросы. Библиотека сама создаёт необходимые классы для выполнения запросов и обрабатывает ответы. Retrofit поддерживает множество функций, среди которых аутентификация и авторизация, поддержка JSON и XML, поддержка многопоточности и многое другое. Основана на библиотеке OkHttp, работает на Java 8 и выше, Android API 21 (Android 5) и выше.
Общие сведения
Описание
Для работы с Retrofit необходимы три вещи:
- класс, который используется как модель получаемого ответа (POJO, можно сгенерировать плагином JSON to Kotlin Class или на сайте jsonschema2pojo);
- интерфейс, который определяет возможные запросы. Каждая его функция представляет из себя один из возможных вызовов API. Всегда возвращает
Call
-объект с типом ожидаемого результата (POJO). Если тип ответа не интересует - можно указатьCall<Responce>
. Перед функцией указывается аннотация с типом запроса (@GET
,@POST
и так далее) и параметром (адресом метода API). В параметрах самой функции перечисляются параметры запроса. Динамические параметры (которые могут изменяться в процессе работы приложения) следует указывать в фигурных скобках, например/users/{user}/repositories
; - экземпляр класса
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
;
- Gson
- Protocol Buffers:
- Protobuf
com.squareup.retrofit2:converter-protobuf
; - Wire
com.squareup.retrofit2:converter-wire
;
- Protobuf
- XML
- Simple XML:
com.squareup.retrofit2:converter-simplexml
- JAXB:
com.squareup.retrofit2:converter-jaxb
- Simple XML:
- Примитивные типы данных
- Scalars (primitives, boxed, and String):
com.squareup.retrofit2:converter-scalars
- Scalars (primitives, boxed, and String):
Конвертер не включен в состав 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()
- полный ответ.