I’m using a custom Interceptor
along with Retrofit client in my Android app, that throws an Exception under some specific circumstances. I’m trying to make it work using Kotlin coroutines.
The problem is that I’m unable to handle the before mentioned error, since in the moment the exception is thrown from within the Interceptor instance, it crashes the whole app instead of being caught in the coroutine’s try/catch
statement. While I was using the Rx implementation, the exception was flawlessly propagated to the onError
callback where I was able to handle it the way I needed.
I guess this is somehow related to the underlying threads that are being used for the network call, please see the logs below from the place where the call is made, from the interceptor just before throwing the exception, and the stacktrace:
2019-11-04 17:17:34.515 29549-29729/com.app W/TAG: Running thread: DefaultDispatcher-worker-1
2019-11-04 17:17:45.911 29549-29834/com.app W/TAG: Interceptor thread: OkHttp https://some.endpoint.com/...
2019-11-04 17:17:45.917 29549-29834/com.app E/AndroidRuntime: FATAL EXCEPTION: OkHttp Dispatcher
Process: com.app, PID: 29549
com.app.IllegalStateException: Passed refresh token can't be used for refreshing the token.
at com.app.net.AuthInterceptor.intercept(AuthInterceptor.kt:33)
What am I supposed to do in order to be able to catch and handle this exception from the Interceptor correctly? Am I missing something?
Rest Error Interceptor
This interceptor provides an easy way to handle the most common REST HTTP status code as exceptions.
Example
try { repository.makeRestRequest() } catch (throwable: Throwable) { when(throwable) { BadRequest -> { } // Handle the bad request status code InternalServerError -> { } // Handle the internal server error status code Unauthorized -> { } // Handle the unauthorized status code } }
How to
val errorInterceptor = RestErrorInterceptor() val client = OkHttpClient.Builder() .addInterceptor(errorInterceptor) .build()
Download
1- Add the Jitpack Repository in your root build.gradle file:
allprojects { repositories { ... maven { url 'https://jitpack.io' } } }
2- Add the dependency in your project-level build.gradle file:
dependencies {
implementation 'com.github.jeancsanchez:okhttp-rest-error-interceptor:{latest version}'
}
Recently wrote a web crawler, end with Andrew was okhttp3, the development of language and must be kotlin, that very little information in this regard, you write about it, the situation is processing the request failed to prevent flash back!
val client = OkHttpClient.Builder (). cookieJar (cookieJar) .build () // initialization request
val myinfo = FormBody.Builder (). add ( "user", user) .build () // request form
var request = Request.Builder (). url (this.logUrl) .post (myinfo) .build () // setup request
Next is a specific request:
var response = this.client.newCall(request).enqueue(object : Callback {
// here is the enqueue request is an asynchronous process anonymous class can override Callback
// kotlin with anonymous class object: the class name () {} where lambda is obtained parameters to the bracket
override fun onResponse(call: Call, response: Response) {
// successful execution of the request xxx
var resText = response.body()?.string()
var temResText: String? = resText
var doc = Jsoup.parse(temResText)
res = doc.getElementsByTag("script").html().toString()
if (isSuccLogin.containsMatchIn(res)) {
// This is the code after successfully processed the request, transmitting data asynchronously to handle the UI
var msg: Message = Message()
msg.what = 1
var temData = Bundle()
temData.putString("user", user)
temData.putString("pw", pw)
msg.data = temData
hand.sendMessage(msg)
} else {
var msg: Message = Message()
msg.what = 6
hand.sendMessage(msg)
Log.d("re:", res)
}
}
// request failed to perform xxx
override fun onFailure(call: Call, e: IOException) {
var msg: Message = Message()
msg.what = 3
hand.sendMessage(msg)
}
})
In this way, do not worry about the immediate collapse, you can add a try and catch catch. . deal with.
You may catch the exception in your custom Interceptor
and return an empty response with some specific message
and code
. I have implemented a custom Interceptor
to handle the situation like when you do not have or slow internet connection etc… Actually coroutine’s suspend functions throws exception when dealing with network calls. In my experience, you can follow 2 approaches. 1. wrap your all network call in try...catch
or 2. create a custom Interceptor
and handle exceptions there and return some specific response.
Approach 1:
try {
webservice.login(username, password)
} catch (e: Exception) {
//...
}
Approach 2:
Create a custom Interceptor
and handle exception there.
class LoggingInterceptor : Interceptor {
@Throws(Exception::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
try {
val response = chain.proceed(request)
val bodyString = response.body()!!.string()
return response.newBuilder()
.body(ResponseBody.create(response.body()?.contentType(), bodyString))
.build()
} catch (e: Exception) {
e.printStackTrace()
var msg = ""
when (e) {
is SocketTimeoutException -> {
msg = "Timeout - Please check your internet connection"
}
is UnknownHostException -> {
msg = "Unable to make a connection. Please check your internet"
}
is ConnectionShutdownException -> {
msg = "Connection shutdown. Please check your internet"
}
is IOException -> {
msg = "Server is unreachable, please try again later."
}
is IllegalStateException -> {
msg = "${e.message}"
}
else -> {
msg = "${e.message}"
}
}
return Response.Builder()
.request(request)
.protocol(Protocol.HTTP_1_1)
.code(999)
.message(msg)
.body(ResponseBody.create(null, "{${e}}")).build()
}
}
}
I have created gist for complete implementation of LoggingInterceptor
with print logs of request and response. LoggingInterceptor
You should subclass IOException
and use that to send information from your interceptors to your calling code.
We consider other exceptions like IllegalStateException
to be application crashes and do not send them over thread boundaries because we don’t want to burden most callers with catching them.
Tags:
Exception
Android
Interceptor
Kotlin Coroutines
Okhttp
Related
Retrofit 2
имеет другую концепцию обращения "successful"
запросы, чем Retrofit 1
. ВRetrofit 2
, все requests
которые могут быть выполнены (отправлены в API) и для которых вы получаете response
рассматриваются как "successful"
. Значит, для этихrequests
в onResponse
обратный вызов запущен, и вам нужно вручную проверить, request
на самом деле successful (status 200-299)
или erroneous (status 400-599)
.
Если запрос завершен successfully
, мы можем использовать response object
и делаем все, что хотели. В случае еслиerror
фактически failed (remember, status 400-599)
, мы хотим показать пользователю соответствующую информацию о проблеме.
пример
Error Object
Предположим, ваш API отправляет JSON error body
нравится:
{
statusCode: 409,
message: "Email address already registered"
}
Примечание: вы можете увидеть свойJSON error body
печатая response.errorBody()
Чтобы избежать этого плохого взаимодействия с пользователем, мы отображаем тело ответа на объект Java, представленный следующим классом.
class APIError {
private val statusCode:Int = 0
private val message:String
fun status():Int {
return statusCode
}
fun message():String {
return message
}
}
Error Handler
object ErrorUtils {
fun parseError(response:Response<*>):APIError {
val converter = ServiceGenerator.retrofit()
.responseBodyConverter(APIError::class.java, arrayOfNulls<Annotation>(0))
val error:APIError
try
{
error = converter.convert(response.errorBody())
}
catch (e:IOException) {
return APIError()
}
return error
}
}
Error Handler in Action
Теперь ты справишься error
в API response
с помощью ErrorUtils
как следующее.
val call = service.me()
call.enqueue(object:Callback<User>() {
fun onResponse(call:Call<User>, response:Response<User>) {
if (response.isSuccessful())
{
// use response data and do some fancy stuff :)
}
else
{
// parse the response body …
val error = ErrorUtils.parseError(response)
// … and use it to show error information
// … or just log the issue like we’re doing :)
Log.d("error message", error.message())
}
}
fun onFailure(call:Call<User>, t:Throwable) {
// there is more than just a failing request (like: no internet connection)
}
})
Полный пример с видео здесь — обработка ошибок retrofit-2.
-
Писать впереди
В предыдущей статье были представлены основы некоторых сопрограмм kotlin и реторфита для сетевых запросов, но если мы отключим мобильную сеть во время выполнения предыдущей демонстрации, что произойдет? , Приложение будет аварийно завершено из-за исключений ввода-вывода! Почему? Это связано с тем, что модификация вызывает исключение IOException при выполнении метода excute (), а метод enqueue () — нет, поскольку он помещает обработку исключения IOException в метод onFailure обратного вызова. Поэтому, если нам нужно использовать метод excute (), нам нужно обработать исключение вручную. -
Аномальная классификация
Если результат сетевого запроса не соответствует ожидаемому, мы можем рассматривать его как исключение запроса. Тогда каковы причины исключения запроса? Я делю исключение запроса на три категории:
Первая категория, проблемы с сетью, возможно, сеть устройства не подключена, сигнал подключения слабый, сеть перегружена и т. д., это IOException;
Вторая категория — это проблема подключения к серверу. Все мы знаем, что код, возвращаемый обычным результатом доступа к сети, равен 200. Если он возвращает 502, 404 и т. д., то это сервер. Это может быть проблема с адресом запроса. Это может быть запрос. Проблема с методом или причиной сервера, это также выражается в модернизации;
Третий тип, возвращаемое значение ненормальное, то есть сервер может возвращать вам данные в обычном режиме, но данные не те, что вы хотите, это логическая проблема, возможно, ваши параметры неверны, возможно, внутренняя обработка Нет, это может быть основано на логике самого вашего запроса. Этот тип исключения должен отображаться. Это должно быть определено в соответствии с конкретным документом интерфейса вашего проекта, и вам необходимо самостоятельно оценить результат.
Хорошо, тип установлен, затем сначала покажите его с помощью кода, сначала напишите класс перечисленияenum class ErrorType { NETWORK_ERROR,//Ошибка сети SERVICE_ERROR,// Исключение доступа к серверу RESPONSE_ERROR// Возвращаемое значение запроса ненормально }
Другой класс ошибок используется для инкапсуляции информации об ошибках.
/** * Ответ на ошибку сетевого запроса */ data class ErrorResponse( val errorType:ErrorType,// Тип ошибки val errorTag:String,// Тег ошибки, используемый для определения того, какой запрос неверен val errorCode: String?,//код ошибки val message: String?//Сообщение об ошибке )
-
Создать ApiSerevice
при модернизации необходимо создать класс интерфейса, но мы можем инициализировать этот класс непосредственно в kotlin. В предыдущей статье было показано, как напрямую получить модифицированные экземпляры синглтонов через сопутствующие объекты (фактически Вышеупомянутый интерфейс не может быть настоящим синглтоном, потому что конструктор не может быть частным). На этот раз мы используем функцию invoke () для создания экземпляра модификации. Что касается синглтона, то реальный проект может быть реализован через структуру внедрения зависимостей, но не намного. Сначала произнесите кодinterface ApiService { @POST("versionupdate/getCurrentAppVersion") fun getCurrentAppVersion(@Query("json") json:String) :Call<UpdateResult> @POST("userinfo/signin") fun userLogin(@Query("json") json:String):Call<LoginResult> companion object { // оператор является оператором конструктора, оператор fun invoke () эквивалентен конструктору java operator fun invoke(): ApiService { // Настроить перехватчик, распечатать адрес запроса и результат запроса val paramInterceptor = Interceptor{ chain -> val url = chain.request().url().url().toString() LogUtil.d("Отправить запрос: $ {URLDecoder.decode (url,"utf-8")}") val response = chain.proceed(chain.request()) // Обратите внимание, что response.body.string () не может использоваться здесь напрямую, иначе поток будет закрыт и будет сообщено об исключении val responseBody = response.peekBody(1024*1024) LogUtil.d("Результат запроса:${responseBody.string()}") return@Interceptor response } val okHttpClient = OkHttpClient.Builder() .addInterceptor(paramInterceptor) .build() return Retrofit.Builder() .baseUrl("http://*****/") // Отрывки кода выбраны из моего личного реального проекта, в целях безопасности скрыть baseUrl .addConverterFactory(GsonConverterFactory.create()) .client(okHttpClient) .build() .create(ApiService::class.java) } } }
-
Сетевой запрос
В предыдущей статье я сказал, что настоящий проект не должен напрямую вызывать API сетевого запроса в действии или фрагменте, это необходимо делать в специальном классе сетевых запросов. Почему? Это связано с тем, что, если сетевой запрос напрямую обрабатывается в действии или фрагменте, поскольку это асинхронная операция, существует риск утечки памяти, и она не будет соответствовать принципу разделения. Если сетевой запрос распространяется по всему проекту, это также вызовет трудности при тестировании интерфейса. Итак, нам нужно создать класс сетевого запроса, основанный на принципе развязки, мы сначала создаем интерфейс/** * Интерфейс, используемый для сетевых запросов для получения данных, два метода соответствуют двум вышеупомянутым сетевым запросам */ interface EduNetworkDataSource { // LiveData используется для инкапсуляции ошибки запроса, пользовательский интерфейс определяет, является ли сеть ненормальной, отслеживая это значение val errorResult:LiveData<ErrorResponse> suspend fun fetchCurrentAppVersion(param: GetVersionParam):LiveData<UpdateResult> suspend fun userLogin(param:LoginParam):LiveData<LoginResult> }
Обратите внимание, что все результаты моих сетевых запросов выше инкапсулированы в LiveData, который является объектом, который может отслеживать изменения данных. Я кратко объяснил LiveData и ViewModel в предыдущей статье об архитектуре MVVM. Если вы хотите узнать больше, пожалуйста См. Официальную документацию Google. Проще говоря, когда его значение изменяется, он уведомляет своих наблюдателей и будет отслеживать жизненный цикл пользовательского интерфейса. Причина использования ключевого слова suspend заключается в том, что сетевой запрос является операцией блокировки. , И наши сетевые запросы будут помещены в сопрограмму.
Хорошо, давайте взглянем на конкретный класс реализации запроса./** * Класс реализации запроса сетевого интерфейса */ class EduNetworkDataSourceImpl( private val apiService: ApiService, private val context: Context ) : EduNetworkDataSource { // Запрашиваем неверный результат, здесь SingleLiveData наследуется от LiveData, когда информация об ошибке считывается, информация в ней автоматически очищается, чтобы следующий наблюдатель не прочитал предыдущую информацию об ошибке private val _errorResult = SingleLiveData<ErrorResponse>() / * Это для внешнего использования, значение LiveData не может быть изменено, MutableLiveData может быть изменен, чтобы предотвратить изменение значения другими классами * / override val errorResult: LiveData<ErrorResponse> get() = _errorResult private val gson = Gson() // Получаем последнюю версию с сервера override suspend fun fetchCurrentAppVersion(param: GetVersionParam) = handleRequest("Получить последнюю версию") { apiService.getCurrentAppVersion(gson.toJson(param)).execute() } //Логин пользователя override suspend fun userLogin(param: LoginParam) = handleRequest("Логин пользователя") { apiService.userLogin(gson.toJson(param)).execute() } /** * Единая обработка ошибок запроса и отправка данных в LiveData * Это функция высшего порядка, первый параметр используется для определения того, какой сетевой запрос * Второй параметр - это метод (это похоже на передачу метода в качестве параметра в javaScript) * Этот BaseResult используется для оценки сбоя бизнес-логики и определяется в соответствии с реальной ситуацией в проекте. Следующие «описание» и «код» взяты из BaseResult */ private suspend fun <T : BaseResult> handleRequest( tag: String, action: () -> Response<T> ): LiveData<T> { val liveData = MutableLiveData<T>() withContext(Dispatchers.IO) { try { val response = action() if (response.isSuccessful) { val body = response.body() // Это определяется в соответствии с конкретным протоколом внешнего и внутреннего интерфейса if (body != null && "0" == body.code) { liveData.postValue(body) } else { // Логические исключения определяются документами внешнего и внутреннего интерфейса, специально сформулированными в реальном проекте, и мы отправим результаты исключения context.showToast("$tag отказ:${body?.description}") _errorResult.postValue( ErrorResponse( ErrorType.RESPONSE_ERROR, tag, body?.code, body?.description ) ) } } else { // Это означает, что результат запроса не 200, сервер неисправен, и сообщение об ошибке и код ответа отправляются context.showToast("$tag отказ:${response.code()} - ${response.message()}") _errorResult.postValue( ErrorResponse( ErrorType.SERVICE_ERROR, tag, response.code().toString(), response.message() ) ) } } catch (e: IOException) { // Если есть исключение ввода-вывода, это означает, что есть проблема с сетью, и значение сообщения об ошибке отправляется напрямую e.printStackTrace() LogUtil.e(e.toString()) context.showToast("$tag отказ:$e") _errorResult.postValue( ErrorResponse( ErrorType.NETWORK_ERROR, tag, null, e.toString() ) ) } } return liveData } }
Выше использован SingleLiveData, код выглядит следующим образом
class SingleLiveData<T> : MutableLiveData<T>() { override fun observe(owner: LifecycleOwner, observer: Observer<in T>) { super.observe(owner, Observer { if(it != null){ observer.onChanged(it) postValue(null) } }) } }
-
Использовать сетевой запрос
Прежде всего, при фактической разработке проекта вам не следует напрямую использовать класс NetworkDataSource для выполнения сетевых запросов в классе пользовательского интерфейса (я имею в виду класс пользовательского интерфейса, относящийся к действию или фрагменту), потому что вы запрашиваете Полученные данные могут потребовать логической обработки, ряда преобразований данных, их сохранения в базе данных и т. Д. Операции в классе пользовательского интерфейса сделают класс пользовательского интерфейса большим и сложным, трудным в обслуживании и тестировании. Если ваш проект использует архитектуру MVP, вам следует В Presenter, если вы используете архитектуру MVVM, вы должны сделать это в репозитории. Здесь я кратко продемонстрирую сетевой запрос в репозитории и то, как он передается в пользовательский интерфейс. Фактический процесс передачи данных похож на меня.Предыдущая статьяАрхитектура, упомянутая в
Первый — это класс репозитория.class EduRepositoryImpl( private val eduNetworkDataSource: EduNetworkDataSource ) : EduRepository { private val userinfo = MutableLiveData<Userinfo>() // Получаем информацию о пользователе fun getUserInfo() = userinfo as LiveData<Userinfo> // Логин пользователя, обновление информации о пользователе после входа override suspend fun userLogin(param: LoginParam): LiveData<LoginResult> { return eduNetworkDataSource.userLogin(param).also { loginResultLiveData -> loginResultLiveData.value?.let {loginResult -> userinfo.postValue(loginResult.userinfo) } } } // Получить последнюю версию override suspend fun getCurrentAppVersion(param: GetVersionParam): LiveData<UpdateResult> { return eduNetworkDataSource.fetchCurrentAppVersion(param) } //Сообщение об ошибке override suspend fun getError(): LiveData<ErrorResponse> { return eduNetworkDataSource.errorResult } }
Затем идет класс ViewModel, здесь показано использование только одного интерфейса
class SplashViewModel(private val eduRepository: EduRepository): ViewModel() { suspend fun getCurrentAppVersion(param: GetVersionParam) = eduRepository.getCurrentAppVersion(param) suspend fun getError() = eduRepository.getError() }
Затем класс ViewModelFactory
class SplashViewModelFactory(private val eduRepository: EduRepository): ViewModelProvider.NewInstanceFactory() { override fun <T : ViewModel?> create(modelClass: Class<T>): T { @Suppress("UNCHECKED_CAST") return SplashViewModel(eduRepository) as T } }
Последний — это соответствующий код в классе пользовательского интерфейса. Обратите внимание, что класс пользовательского интерфейса по-прежнему должен наследовать CoroutineScope и инициализировать задание и coroutineContext. Это тот же код, что и в предыдущем коде, поэтому я не буду публиковать его. Подробности см. В предыдущей статье.
launch { viewModel.getCurrentAppVersion(GetVersionParam("1")).observe(this@SplashActivity, Observer { // Доступ к сети прошел успешно, здесь вы получите уведомление об обновлении данных от liveata if (it.versionupdate.versioncode > BuildConfig.VERSION_CODE) { showToast("Есть новая версия${it.versionupdate.versionname}Имеется в наличии") } else { delayToHome() } }) viewModel.getError().observe(this@SplashActivity, Observer { // Сбой доступа к сети, здесь liveata уведомит delayToHome() }) }
Таким образом, в классе пользовательского интерфейса только очень небольшой объем кода может использоваться для обеспечения доступа к сети и мониторинга исключений, и даже если вы не отслеживаете ошибки, программа не выйдет из строя или запустится ненормально, потому что в классе DataSource есть try / catch.
Наконец, все это, будь то сетевой запрос или мониторинг данных, выполняется в пределах диапазона безопасности жизненного цикла пользовательского интерфейса, поскольку Livedata отслеживает жизненный цикл пользовательского интерфейса, как и запуск нашей сопрограммы. Он будет выполняться только в течение жизненного цикла пользовательского интерфейса, и наш наблюдатель прослушивает основной поток и сопрограмму, поэтому вы можете управлять пользовательским интерфейсом и безопасно выполнять трудоемкие операции, не беспокоясь о блокировке потока (конечно, Не рекомендуется выполнять трудоемкие операции в классе UI, потому что класс UI контролируется системой, и ваши трудоемкие операции могут быть прерваны в любое время из-за перезапуска класса UI).
Оригинальная статья, укажите источник для перепечатки, спасибо
Многие сайты имеют собственные API для удобного доступа к своим данным. На данный момент самый распространённый вариант — это JSON. Также могут встречаться данные в виде XML и других форматов.
Библиотека Retrofit упрощает взаимодействие с REST API сайта, беря на себя часть рутинной работы.
Авторами библиотеки Retrofit являются разработчики из компании «Square», которые написали множество полезных библиотек, например, Picasso, Okhttp, Otto.
Домашняя страница — http://square.github.io/retrofit/
Библиотекой удобно пользоваться для запроса к различным веб-сервисам с командами GET, POST, PUT, DELETE. Может работать в асинхронном режиме, что избавляет от лишнего кода.
В основном вам придётся работать с методами GET и POST. Если вы будет создавать собственный API, то будете использовать и другие команды.
Подключается стандартно.
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
В Retrofit 2.x автоматически подключается библиотека OkHttp и её не нужно прописывать отдельно.
Библиотека может работать с GSON и XML, используя специальные конвертеры, которые следует указать отдельно.
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
Затем в коде конвертер добавляется с помощью метода addConverterFactory().
addConverterFactory(GsonConverterFactory.create()
Список готовых конвертеров:
- Gson: com.squareup.retrofit2:converter-gson
- Jackson: com.squareup.retrofit2:converter-jackson
- Moshi: com.squareup.retrofit2:converter-moshi
- Protobuf: com.squareup.retrofit2:converter-protobuf
- Wire: com.squareup.retrofit2:converter-wire
- Simple XML: com.squareup.retrofit2:converter-simplexml
- Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars
Также вы можете создать свой собственный конвертер, реализовав интерфейс на основе абстрактного класса Converter.Factory.
Можно подключить несколько конвертеров (порядок важен).
Retrofit retrofit = Retrofit.Builder()
.baseUrl("https://your.api.url/v2/");
.addConverterFactory(ProtoConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build();
Если вы хотите изменить формат какого-нибудь JSON-объекта, то это можно сделать с помощью GsonConverterFactory.create():
Gson gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/base/")
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
service = retrofit.create(APIService.class);
Базовый URL всегда заканчивается слешем /. Задаётся в методе baseUrl().
Можно указать полный URL в запросе, тогда базовый URL будет проигнорирован:
public interface APIService {
@GET("https://api.sample.com/users/user/list")
Call<Users> getUsers();
}
Для работы с Retrofit понадобятся три класса.
- POJO (Plain Old Java Object) или Model Class — json-ответ от сервера нужно реализовать как модель
- Retrofit — класс для обработки результатов. Ему нужно указать базовый адрес в методе baseUrl()
- Interface — интерфейс для управления адресом, используя команды GET, POST и т.д.
Работу с Retrofit можно разбить на отдельные задачи.
Задача первая. POJO
Задача первая — посмотреть на структуру ответа сайта в виде JSON (или других форматов) и создать на его основе Java-класс в виде POJO.
POJO удобнее создавать с помощью готовых веб-сервисов в автоматическом режиме. Либо можете самостоятельно создать класс, если структура не слишком сложная.
В классе часто используются аннотации. Иногда они необходимы, иногда их можно пропустить. В некоторых случаях аннотации помогают избежать ошибок. Список аннотаций зависит от типа используемого конвертера, их список можно посмотреть в соответствующей документации.
Задача вторая. Интерфейс
Задача вторая — создать интерфейс и указать имя метода. Добавить необходимые параметры, если они требуются.
В интерфейсе задаются команды-запросы для сервера. Команда комбинируется с базовым адресом сайта (baseUrl()) и получается полный путь к странице. Код может быть простым и сложным. Можно посмотреть примеры в документации.
Запросы размещаются в обобщённом классе Call с указанием желаемого типа.
import retrofit2.Call;
public interface APIService {
@POST("list")
Call<Repo> loadRepo();
}
В большинстве случаев вы будете возвращать объект Call<T> с нужным типом, например, Call<User>. Если вас не интересует тип ответа, то можете указать Call<Response>.
Здесь также используются аннотации, но уже от самой библиотеки.
С помощью аннотации указываются веб-команды, а затем Java-метод. Для динамических параметров используются фигурные скобки (users/{user}/repos), в которые подставляются нужные значения.
В самой аннотации используется метод, используемый на сервере, а ниже вы можете указать свой вариант (полезно для соответствия стилю вашего кода.
@GET("get_all_cats") // команда на сервере
List<Cat> getAllCats(); // ваш код
Аннотации
Аннотация | Описание |
---|---|
@GET() | GET-запрос для базового адреса. Также можно указать параметры в скобках |
@POST() | POST-запрос для базового адреса. Также можно указать параметры в скобках |
@Path | Переменная для замещения конечной точки, например, username подставится в {username} в адресе конечной точки |
@Query | Задаёт имя ключа запроса со значением параметра |
@Body | Используется в POST-вызовах (из Java-объекта в JSON-строку) |
@Header | Задаёт заголовок со значением параметра |
@Headers | Задаёт все заголовки вместе |
@Multipart | Используется при загрузке файлов или изображений |
@FormUrlEncoded | Используется при использовании пары «имя/значение» в POST-запросах |
@FieldMap | Используется при использовании пары «имя/значение» в POST-запросах |
@Url | Для поддержки динамических адресов |
@Query
Аннотация @Query полезна при запросах с параметрами. Допустим, у сайте есть дополнительный параметр к запросу, который выводит список элементов в отсортированном виде: http://example.com/api/v1/products/cats?sort=desc. Это несложный пример и мы можем поместить запрос с параметром в интерфейс без изменений.
@GET("products/cats?category=5&sort=desc")
Call<Cats> getAllCats();
Если не требуется управлять сортировкой, то её можно оставить в коде и она будет применяться по умолчанию. Но в нашем запросе есть ещё один параметр, который отвечает за категорию котов (домашние, уличные, породистые), которая может меняться в зависимости от логики приложения. Этот параметр можно снабдить аннотацией и программно управлять в коде.
@GET("products/cats?sort=desc")
Call<Cats> getAllCats(@Query("category") int categoryId);
Сортировку мы оставляем как есть, а категорию перенесли в параметры метода под именем categoryId, снабдив аннотацией, с которой параметр будет обращаться на сервер в составе запроса.
Call<Cats> getAllCats() = catAPIService.getAllCats(5);
Запрос получится в виде http://example.com/api/v1/products/cats?sort=desc&category=5.
В одном методе можно указать несколько Query-параметров.
@Path
Запрос может иметь изменяемые части пути. Посмотрите на один из примеров запроса для GitHub: /users/:username. Вместо :username следует подставлять конкретные имена пользователей (https://api.github.com/users/alexanderklimov). В таких случаях используют фигурные скобки в запросе, в самоме методе через аннотацию @Path указывается имя, которое будет подставляться в путь.
@GET("/users/{username}")
Call getUser(
@Path("username") String userName
);
@Headers
Пример аннотации @Headers, которая позволяет указать все заголовки вместе.
@Headers({"Cache-Control: max-age=640000", "User-Agent: My-App-Name"})
@GET("some/endpoint")
@Multipart
Пример аннотации @Multipart при загрузке файлов или картинок:
@Multipart
@POST("some/endpoint")
Call<Response> uploadImage(@Part("description") String description, @Part("image") RequestBody image)
@FormUrlEncoded
Пример использования аннотации @FormUrlEncoded:
@FormUrlEncoded
@POST("/some/endpoint")
Call<SomeResponse> someEndpoint(@FieldMap Map<String, String> names);
@Url
Пример аннотации @Url:
public interface UserService {
@GET
public Call<File> getZipFile(@Url String url);
}
Задача третья. Retrofit
Для синхронного запроса используйте метод Call.execute(), для асинхронного — метод Call.enqueue().
Объект для запроса к серверу создаётся в простейшем случае следующим образом
public static final String BASE_URL = "http://api.example.com/";
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
В итоге мы получили объект Retrofit, содержащий базовый URL и способность преобразовывать JSON-данные с помощью указанного конвертера Gson.
Далее в его методе create() указываем наш класс интерфейса с запросами к сайту.
UserService userService = retrofit.create(UserService.class);
После этого мы получаем объект Call и вызываем метод enqueue() (для асинхронного вызова) и создаём для него Callback. Запрос будет выполнен в отдельном потоке, а результат придет в Callback в main-потоке.
В результате библиотека Retrofit сделает запрос, получит ответ и производёт разбор ответа, раскладывая по полочкам данные. Вам остаётся только вызывать нужные методы класса-модели для извлечения данных.
Основная часть работы происходит в onResponse(), ошибки выводятся в onFailure() (неправильный адрес сервера, некорректные формат данных, неправильный формат класса-модели и т.п). HTTP-коды сервера (например, 404) не относятся к ошибкам.
Метод onResponse() вызывается всегда, даже если запрос был неуспешным. Класс Response имеет удобный метод isSuccessful() для успешной обработки запроса (коды 200хх). В ошибочных ситуациях вы можете обработать ошибку в методе errorBody() класса ResponseBody.
Другие полезные методы Response.
- code() — HTTP-код ответа
- body() — сам ответ в виде строки, без сериализации
- headers() — HTTP-заголовки
- message() — HTTP-статус (или null)
- raw() — сырой HTTP-ответ
Можно написать такую конструкцию.
// код 200
if (response.isSuccessful()) {
... // код для успешного случая
} else {
switch(response.code()) {
case 404:
// страница не найдена. можно использовать ResponseBody, см. ниже
break;
case 500:
// ошибка на сервере. можно использовать ResponseBody, см. ниже
break;
}
// или
// Также можете использовать ResponseBody для получения текста ошибки
ResponseBody errorBody = response.errorBody();
try {
mTextView.setText(errorBody.string());
} catch (IOException e) {
e.printStackTrace();
}
}
Для отмены запроса используется метод Call.cancel().
Перехватчики (Interceptors)
В библиотеку можно внедрить перехватчики для изменения заголовков при помощи класса Interceptor из OkHttp. Сначала следует создать объект перехватчика и передать его в OkHttp, который в свою очередь следует явно подключить в Retrofit.Builder через метод client().
Поддержка перехватчиков/interceptors для обработки заголовков запросов, например, для работы с токенами авторизации в заголовке Authorization.
OkHttpClient client = new OkHttpClient();
client.interceptors().add(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
// Настраиваем запросы
Request request = original.newBuilder()
.header("Accept", "application/json")
.header("Authorization", "auth-token")
.method(original.method(), original.body())
.build();
Response response = chain.proceed(request);
return response;
}
});
Retrofit retrofit = Retrofit.Builder()
.baseUrl("https://your.api.url/v2/");
.client(client)
.build();
HttpLoggingInterceptor
Библиотека HttpLoggingInterceptor является частью OkHttp, но поставляется отдельно от неё. Перехватчик следует использовать в том случае, когда вам действительно нужно изучать логи ответов сервера. По сути библиотека является сетевым аналогом привычного LogCat.
Подключаем библиотеку.
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0'
Подключаем перехватчик к веб-клиенту. Добавляйте его после других перехватчиков, чтобы ловить все сообщения. Существует несколько уровней перехвата данных: NONE, BASIC, HEADERS, BODY. Последний вариант самый информативный, пользуйтесь им осторожно. При больших потоках данных информация забьёт весь экран. Используйте промежуточные варианты.
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
// Только в режиме отладки
if(BuildConfig.DEBUG){
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY );
}
OkHttpClient okClient = new OkHttpClient.Builder()
.addInterceptor(new ResponseInterceptor())
.addInterceptor(loggingInterceptor)
.build();
RxJava
Сами разработчики библиотеки очень любят реактивное программирование и приложили многие усилия для интеграции с библиотекой RxJava.
// build.gradle
implementation 'com.squareup.retrofit2:adapter-rxjava:2.5.0'
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(baseUrl);
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build();
Дополнительное чтение
Retrofit 2.x. Примеры для GitHub
Retrofit 2.x. Другие примеры
Retrofit 2.x. POST, PUT, DELETE
Retrofit 2.x. Конвертер XML
Retrofit 2.x. Конвертер ScalarsConverterFactory и метод POST
Курс валют Центрального Банка России. Retrofit, XML
Retrofit на Kotlin с применением корутины. JSONPlaceholder
Retrofit, Reddit (Kotlin)
TheCatAPI — Cats as a Service, Everyday is Caturday (Retrofit, Kotlin)
Реклама
Retrofit 2
has a different concept of handling "successful"
requests than Retrofit 1
. In Retrofit 2
, all requests
that can be executed (sent to the API) and for which you’re receiving a response
are seen as "successful"
. That means, for these requests
the onResponse
callback is fired and you need to manually check whether the request
is actually successful (status 200-299)
or erroneous (status 400-599)
.
If the request finished successfully
, we can use the response object
and do whatever we wanted. In case the error
actually failed (remember, status 400-599)
, we want to show the user appropriate information about the issue.
Example
Error Object
Let’s assume your API sends a JSON error body
like this:
{
statusCode: 409,
message: "Email address already registered"
}
Note: you can see your JSON error body
by printing response.errorBody()
To avoid these bad user experiences, we’re mapping the response body to a Java object, represented by the following class.
class APIError {
private val statusCode:Int = 0
private val message:String
fun status():Int {
return statusCode
}
fun message():String {
return message
}
}
Error Handler
object ErrorUtils {
fun parseError(response:Response<*>):APIError {
val converter = ServiceGenerator.retrofit()
.responseBodyConverter(APIError::class.java, arrayOfNulls<Annotation>(0))
val error:APIError
try
{
error = converter.convert(response.errorBody())
}
catch (e:IOException) {
return APIError()
}
return error
}
}
Error Handler in Action
Now you can handle error
in API response
using ErrorUtils
like the following.
val call = service.me()
call.enqueue(object:Callback<User>() {
fun onResponse(call:Call<User>, response:Response<User>) {
if (response.isSuccessful())
{
// use response data and do some fancy stuff :)
}
else
{
// parse the response body …
val error = ErrorUtils.parseError(response)
// … and use it to show error information
// … or just log the issue like we’re doing :)
Log.d("error message", error.message())
}
}
fun onFailure(call:Call<User>, t:Throwable) {
// there is more than just a failing request (like: no internet connection)
}
})
The complete example with a video is here retrofit-2-error-handling.
We’ve written some recipes that demonstrate how to solve common problems with OkHttp. Read through them to learn about how everything works together. Cut-and-paste these examples freely; that’s what they’re for.
Synchronous Get (.kt, .java)¶
Download a file, print its headers, and print its response body as a string.
The string()
method on response body is convenient and efficient for small documents. But if the response body is large (greater than 1 MiB), avoid string()
because it will load the entire document into memory. In that case, prefer to process the body as a stream.
Kotlin Java
private val client = OkHttpClient()
fun run() {
val request = Request.Builder()
.url("https://publicobject.com/helloworld.txt")
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
for ((name, value) in response.headers) {
println("$name: $value")
}
println(response.body!!.string())
}
}
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://publicobject.com/helloworld.txt")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
Headers responseHeaders = response.headers();
for (int i = 0; i < responseHeaders.size(); i++) {
System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
}
System.out.println(response.body().string());
}
}
Asynchronous Get (.kt, .java)¶
Download a file on a worker thread, and get called back when the response is readable. The callback is made after the response headers are ready. Reading the response body may still block. OkHttp doesn’t currently offer asynchronous APIs to receive a response body in parts.
Kotlin Java
private val client = OkHttpClient()
fun run() {
val request = Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
e.printStackTrace()
}
override fun onResponse(call: Call, response: Response) {
response.use {
if (!response.isSuccessful) throw IOException("Unexpected code $response")
for ((name, value) in response.headers) {
println("$name: $value")
}
println(response.body!!.string())
}
}
})
}
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
client.newCall(request).enqueue(new Callback() {
@Override public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
@Override public void onResponse(Call call, Response response) throws IOException {
try (ResponseBody responseBody = response.body()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
Headers responseHeaders = response.headers();
for (int i = 0, size = responseHeaders.size(); i < size; i++) {
System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
}
System.out.println(responseBody.string());
}
}
});
}
Typically HTTP headers work like a Map<String, String>
: each field has one value or none. But some headers permit multiple values, like Guava’s Multimap. For example, it’s legal and common for an HTTP response to supply multiple Vary
headers. OkHttp’s APIs attempt to make both cases comfortable.
When writing request headers, use header(name, value)
to set the only occurrence of name
to value
. If there are existing values, they will be removed before the new value is added. Use addHeader(name, value)
to add a header without removing the headers already present.
When reading response a header, use header(name)
to return the last occurrence of the named value. Usually this is also the only occurrence! If no value is present, header(name)
will return null. To read all of a field’s values as a list, use headers(name)
.
To visit all headers, use the Headers
class which supports access by index.
Kotlin Java
private val client = OkHttpClient()
fun run() {
val request = Request.Builder()
.url("https://api.github.com/repos/square/okhttp/issues")
.header("User-Agent", "OkHttp Headers.java")
.addHeader("Accept", "application/json; q=0.5")
.addHeader("Accept", "application/vnd.github.v3+json")
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
println("Server: ${response.header("Server")}")
println("Date: ${response.header("Date")}")
println("Vary: ${response.headers("Vary")}")
}
}
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/repos/square/okhttp/issues")
.header("User-Agent", "OkHttp Headers.java")
.addHeader("Accept", "application/json; q=0.5")
.addHeader("Accept", "application/vnd.github.v3+json")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println("Server: " + response.header("Server"));
System.out.println("Date: " + response.header("Date"));
System.out.println("Vary: " + response.headers("Vary"));
}
}
Posting a String (.kt, .java)¶
Use an HTTP POST to send a request body to a service. This example posts a markdown document to a web service that renders markdown as HTML. Because the entire request body is in memory simultaneously, avoid posting large (greater than 1 MiB) documents using this API.
Kotlin Java
private val client = OkHttpClient()
fun run() {
val postBody = """
|Releases
|--------
|
| * _1.0_ May 6, 2013
| * _1.1_ June 15, 2013
| * _1.2_ August 11, 2013
|""".trimMargin()
val request = Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(postBody.toRequestBody(MEDIA_TYPE_MARKDOWN))
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
println(response.body!!.string())
}
}
companion object {
val MEDIA_TYPE_MARKDOWN = "text/x-markdown; charset=utf-8".toMediaType()
}
public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
String postBody = ""
+ "Releasesn"
+ "--------n"
+ "n"
+ " * _1.0_ May 6, 2013n"
+ " * _1.1_ June 15, 2013n"
+ " * _1.2_ August 11, 2013n";
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
Post Streaming (.kt, .java)¶
Here we POST
a request body as a stream. The content of this request body is being generated as it’s being written. This example streams directly into the Okio buffered sink. Your programs may prefer an OutputStream
, which you can get from BufferedSink.outputStream()
.
Kotlin Java
private val client = OkHttpClient()
fun run() {
val requestBody = object : RequestBody() {
override fun contentType() = MEDIA_TYPE_MARKDOWN
override fun writeTo(sink: BufferedSink) {
sink.writeUtf8("Numbersn")
sink.writeUtf8("-------n")
for (i in 2..997) {
sink.writeUtf8(String.format(" * $i = ${factor(i)}n"))
}
}
private fun factor(n: Int): String {
for (i in 2 until n) {
val x = n / i
if (x * i == n) return "${factor(x)} × $i"
}
return n.toString()
}
}
val request = Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(requestBody)
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
println(response.body!!.string())
}
}
companion object {
val MEDIA_TYPE_MARKDOWN = "text/x-markdown; charset=utf-8".toMediaType()
}
public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
RequestBody requestBody = new RequestBody() {
@Override public MediaType contentType() {
return MEDIA_TYPE_MARKDOWN;
}
@Override public void writeTo(BufferedSink sink) throws IOException {
sink.writeUtf8("Numbersn");
sink.writeUtf8("-------n");
for (int i = 2; i <= 997; i++) {
sink.writeUtf8(String.format(" * %s = %sn", i, factor(i)));
}
}
private String factor(int n) {
for (int i = 2; i < n; i++) {
int x = n / i;
if (x * i == n) return factor(x) + " × " + i;
}
return Integer.toString(n);
}
};
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(requestBody)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
Posting a File (.kt, .java)¶
It’s easy to use a file as a request body.
Kotlin Java
private val client = OkHttpClient()
fun run() {
val file = File("README.md")
val request = Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(file.asRequestBody(MEDIA_TYPE_MARKDOWN))
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
println(response.body!!.string())
}
}
companion object {
val MEDIA_TYPE_MARKDOWN = "text/x-markdown; charset=utf-8".toMediaType()
}
public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
File file = new File("README.md");
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
Posting form parameters (.kt, .java)¶
Use FormBody.Builder
to build a request body that works like an HTML <form>
tag. Names and values will be encoded using an HTML-compatible form URL encoding.
Kotlin Java
private val client = OkHttpClient()
fun run() {
val formBody = FormBody.Builder()
.add("search", "Jurassic Park")
.build()
val request = Request.Builder()
.url("https://en.wikipedia.org/w/index.php")
.post(formBody)
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
println(response.body!!.string())
}
}
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
RequestBody formBody = new FormBody.Builder()
.add("search", "Jurassic Park")
.build();
Request request = new Request.Builder()
.url("https://en.wikipedia.org/w/index.php")
.post(formBody)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
Posting a multipart request (.kt, .java)¶
MultipartBody.Builder
can build sophisticated request bodies compatible with HTML file upload forms. Each part of a multipart request body is itself a request body, and can define its own headers. If present, these headers should describe the part body, such as its Content-Disposition
. The Content-Length
and Content-Type
headers are added automatically if they’re available.
Kotlin Java
private val client = OkHttpClient()
fun run() {
// Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("title", "Square Logo")
.addFormDataPart("image", "logo-square.png",
File("docs/images/logo-square.png").asRequestBody(MEDIA_TYPE_PNG))
.build()
val request = Request.Builder()
.header("Authorization", "Client-ID $IMGUR_CLIENT_ID")
.url("https://api.imgur.com/3/image")
.post(requestBody)
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
println(response.body!!.string())
}
}
companion object {
/**
* The imgur client ID for OkHttp recipes. If you're using imgur for anything other than running
* these examples, please request your own client ID! https://api.imgur.com/oauth2
*/
private val IMGUR_CLIENT_ID = "9199fdef135c122"
private val MEDIA_TYPE_PNG = "image/png".toMediaType()
}
/**
* The imgur client ID for OkHttp recipes. If you're using imgur for anything other than running
* these examples, please request your own client ID! https://api.imgur.com/oauth2
*/
private static final String IMGUR_CLIENT_ID = "...";
private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
// Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("title", "Square Logo")
.addFormDataPart("image", "logo-square.png",
RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
.build();
Request request = new Request.Builder()
.header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
.url("https://api.imgur.com/3/image")
.post(requestBody)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
Parse a JSON Response With Moshi (.kt, .java)¶
Moshi is a handy API for converting between JSON and Java objects. Here we’re using it to decode a JSON response from a GitHub API.
Note that ResponseBody.charStream()
uses the Content-Type
response header to select which charset to use when decoding the response body. It defaults to UTF-8
if no charset is specified.
Kotlin Java
private val client = OkHttpClient()
private val moshi = Moshi.Builder().build()
private val gistJsonAdapter = moshi.adapter(Gist::class.java)
fun run() {
val request = Request.Builder()
.url("https://api.github.com/gists/c2a7c39532239ff261be")
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
val gist = gistJsonAdapter.fromJson(response.body!!.source())
for ((key, value) in gist!!.files!!) {
println(key)
println(value.content)
}
}
}
@JsonClass(generateAdapter = true)
data class Gist(var files: Map<String, GistFile>?)
@JsonClass(generateAdapter = true)
data class GistFile(var content: String?)
private final OkHttpClient client = new OkHttpClient();
private final Moshi moshi = new Moshi.Builder().build();
private final JsonAdapter<Gist> gistJsonAdapter = moshi.adapter(Gist.class);
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/gists/c2a7c39532239ff261be")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
Gist gist = gistJsonAdapter.fromJson(response.body().source());
for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
System.out.println(entry.getKey());
System.out.println(entry.getValue().content);
}
}
}
static class Gist {
Map<String, GistFile> files;
}
static class GistFile {
String content;
}
Response Caching (.kt, .java)¶
To cache responses, you’ll need a cache directory that you can read and write to, and a limit on the cache’s size. The cache directory should be private, and untrusted applications should not be able to read its contents!
It is an error to have multiple caches accessing the same cache directory simultaneously. Most applications should call new OkHttpClient()
exactly once, configure it with their cache, and use that same instance everywhere. Otherwise the two cache instances will stomp on each other, corrupt the response cache, and possibly crash your program.
Response caching uses HTTP headers for all configuration. You can add request headers like Cache-Control: max-stale=3600
and OkHttp’s cache will honor them. Your webserver configures how long responses are cached with its own response headers, like Cache-Control: max-age=9600
. There are cache headers to force a cached response, force a network response, or force the network response to be validated with a conditional GET.
Kotlin Java
private val client: OkHttpClient = OkHttpClient.Builder()
.cache(Cache(
directory = cacheDirectory,
maxSize = 10L * 1024L * 1024L // 10 MiB
))
.build()
fun run() {
val request = Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build()
val response1Body = client.newCall(request).execute().use {
if (!it.isSuccessful) throw IOException("Unexpected code $it")
println("Response 1 response: $it")
println("Response 1 cache response: ${it.cacheResponse}")
println("Response 1 network response: ${it.networkResponse}")
return@use it.body!!.string()
}
val response2Body = client.newCall(request).execute().use {
if (!it.isSuccessful) throw IOException("Unexpected code $it")
println("Response 2 response: $it")
println("Response 2 cache response: ${it.cacheResponse}")
println("Response 2 network response: ${it.networkResponse}")
return@use it.body!!.string()
}
println("Response 2 equals Response 1? " + (response1Body == response2Body))
}
private final OkHttpClient client;
public CacheResponse(File cacheDirectory) throws Exception {
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(cacheDirectory, cacheSize);
client = new OkHttpClient.Builder()
.cache(cache)
.build();
}
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
String response1Body;
try (Response response1 = client.newCall(request).execute()) {
if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);
response1Body = response1.body().string();
System.out.println("Response 1 response: " + response1);
System.out.println("Response 1 cache response: " + response1.cacheResponse());
System.out.println("Response 1 network response: " + response1.networkResponse());
}
String response2Body;
try (Response response2 = client.newCall(request).execute()) {
if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);
response2Body = response2.body().string();
System.out.println("Response 2 response: " + response2);
System.out.println("Response 2 cache response: " + response2.cacheResponse());
System.out.println("Response 2 network response: " + response2.networkResponse());
}
System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
}
To prevent a response from using the cache, use CacheControl.FORCE_NETWORK
. To prevent it from using the network, use CacheControl.FORCE_CACHE
. Be warned: if you use FORCE_CACHE
and the response requires the network, OkHttp will return a 504 Unsatisfiable Request
response.
Canceling a Call (.kt, .java)¶
Use Call.cancel()
to stop an ongoing call immediately. If a thread is currently writing a request or reading a response, it will receive an IOException
. Use this to conserve the network when a call is no longer necessary; for example when your user navigates away from an application. Both synchronous and asynchronous calls can be canceled.
Kotlin Java
private val executor = Executors.newScheduledThreadPool(1)
private val client = OkHttpClient()
fun run() {
val request = Request.Builder()
.url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
.build()
val startNanos = System.nanoTime()
val call = client.newCall(request)
// Schedule a job to cancel the call in 1 second.
executor.schedule({
System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f)
call.cancel()
System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f)
}, 1, TimeUnit.SECONDS)
System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f)
try {
call.execute().use { response ->
System.out.printf("%.2f Call was expected to fail, but completed: %s%n",
(System.nanoTime() - startNanos) / 1e9f, response)
}
} catch (e: IOException) {
System.out.printf("%.2f Call failed as expected: %s%n",
(System.nanoTime() - startNanos) / 1e9f, e)
}
}
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
.build();
final long startNanos = System.nanoTime();
final Call call = client.newCall(request);
// Schedule a job to cancel the call in 1 second.
executor.schedule(new Runnable() {
@Override public void run() {
System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f);
call.cancel();
System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f);
}
}, 1, TimeUnit.SECONDS);
System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f);
try (Response response = call.execute()) {
System.out.printf("%.2f Call was expected to fail, but completed: %s%n",
(System.nanoTime() - startNanos) / 1e9f, response);
} catch (IOException e) {
System.out.printf("%.2f Call failed as expected: %s%n",
(System.nanoTime() - startNanos) / 1e9f, e);
}
}
Timeouts (.kt, .java)¶
Use timeouts to fail a call when its peer is unreachable. Network partitions can be due to client connectivity problems, server availability problems, or anything between. OkHttp supports connect, write, read, and full call timeouts.
Kotlin Java
private val client: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.callTimeout(10, TimeUnit.SECONDS)
.build()
fun run() {
val request = Request.Builder()
.url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
.build()
client.newCall(request).execute().use { response ->
println("Response completed: $response")
}
}
private final OkHttpClient client;
public ConfigureTimeouts() throws Exception {
client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
}
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
.build();
try (Response response = client.newCall(request).execute()) {
System.out.println("Response completed: " + response);
}
}
Per-call Configuration (.kt, .java)¶
All the HTTP client configuration lives in OkHttpClient
including proxy settings, timeouts, and caches. When you need to change the configuration of a single call, call OkHttpClient.newBuilder()
. This returns a builder that shares the same connection pool, dispatcher, and configuration with the original client. In the example below, we make one request with a 500 ms timeout and another with a 3000 ms timeout.
Kotlin Java
private val client = OkHttpClient()
fun run() {
val request = Request.Builder()
.url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay.
.build()
// Copy to customize OkHttp for this request.
val client1 = client.newBuilder()
.readTimeout(500, TimeUnit.MILLISECONDS)
.build()
try {
client1.newCall(request).execute().use { response ->
println("Response 1 succeeded: $response")
}
} catch (e: IOException) {
println("Response 1 failed: $e")
}
// Copy to customize OkHttp for this request.
val client2 = client.newBuilder()
.readTimeout(3000, TimeUnit.MILLISECONDS)
.build()
try {
client2.newCall(request).execute().use { response ->
println("Response 2 succeeded: $response")
}
} catch (e: IOException) {
println("Response 2 failed: $e")
}
}
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay.
.build();
// Copy to customize OkHttp for this request.
OkHttpClient client1 = client.newBuilder()
.readTimeout(500, TimeUnit.MILLISECONDS)
.build();
try (Response response = client1.newCall(request).execute()) {
System.out.println("Response 1 succeeded: " + response);
} catch (IOException e) {
System.out.println("Response 1 failed: " + e);
}
// Copy to customize OkHttp for this request.
OkHttpClient client2 = client.newBuilder()
.readTimeout(3000, TimeUnit.MILLISECONDS)
.build();
try (Response response = client2.newCall(request).execute()) {
System.out.println("Response 2 succeeded: " + response);
} catch (IOException e) {
System.out.println("Response 2 failed: " + e);
}
}
Handling authentication (.kt, .java)¶
OkHttp can automatically retry unauthenticated requests. When a response is 401 Not Authorized
, an Authenticator
is asked to supply credentials. Implementations should build a new request that includes the missing credentials. If no credentials are available, return null to skip the retry.
Use Response.challenges()
to get the schemes and realms of any authentication challenges. When fulfilling a Basic
challenge, use Credentials.basic(username, password)
to encode the request header.
Kotlin Java
private val client = OkHttpClient.Builder()
.authenticator(object : Authenticator {
@Throws(IOException::class)
override fun authenticate(route: Route?, response: Response): Request? {
if (response.request.header("Authorization") != null) {
return null // Give up, we've already attempted to authenticate.
}
println("Authenticating for response: $response")
println("Challenges: ${response.challenges()}")
val credential = Credentials.basic("jesse", "password1")
return response.request.newBuilder()
.header("Authorization", credential)
.build()
}
})
.build()
fun run() {
val request = Request.Builder()
.url("http://publicobject.com/secrets/hellosecret.txt")
.build()
}
To avoid making many retries when authentication isn’t working, you can return null to give up. For example, you may want to skip the retry when these exact credentials have already been attempted:
if (credential == response.request.header("Authorization")) {
return null // If we already failed with these credentials, don't retry.
}
You may also skip the retry when you’ve hit an application-defined attempt limit:
if (response.responseCount >= 3) {
return null // If we've failed 3 times, give up.
}
This above code relies on this responseCount
extension val:
val Response.responseCount: Int
get() = generateSequence(this) { it.priorResponse }.count()
private final OkHttpClient client;
public Authenticate() {
client = new OkHttpClient.Builder()
.authenticator(new Authenticator() {
@Override public Request authenticate(Route route, Response response) throws IOException {
if (response.request().header("Authorization") != null) {
return null; // Give up, we've already attempted to authenticate.
}
System.out.println("Authenticating for response: " + response);
System.out.println("Challenges: " + response.challenges());
String credential = Credentials.basic("jesse", "password1");
return response.request().newBuilder()
.header("Authorization", credential)
.build();
}
})
.build();
}
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/secrets/hellosecret.txt")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
To avoid making many retries when authentication isn’t working, you can return null to give up. For example, you may want to skip the retry when these exact credentials have already been attempted:
if (credential.equals(response.request().header("Authorization"))) {
return null; // If we already failed with these credentials, don't retry.
}
You may also skip the retry when you’ve hit an application-defined attempt limit:
if (responseCount(response) >= 3) {
return null; // If we've failed 3 times, give up.
}
This above code relies on this responseCount()
method:
private int responseCount(Response response) {
int result = 1;
while ((response = response.priorResponse()) != null) {
result++;
}
return result;
}