Обработать ошибку от сервера okhttp kotlin

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}'
}
  • Писать впереди
    В предыдущей статье были представлены основы некоторых сопрограмм 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 понадобятся три класса.

  1. POJO (Plain Old Java Object) или Model Class — json-ответ от сервера нужно реализовать как модель
  2. Retrofit — класс для обработки результатов. Ему нужно указать базовый адрес в методе baseUrl()
  3. 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)

Реклама

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;
  }

Уровень сложности
Простой

Время на прочтение
17 мин

Количество просмотров 7.7K

OkHttp — библиотека и по совместительству HTTP‑клиент с открытым исходным кодом для Java и Kotlin, разработанная Square, которая также создала Retrofit.

OkHttp предоставляет простой, легкий в использовании API для выполнения HTTP‑запросов, включая поддержку протоколов HTTP/1.1 и HTTP/2. Библиотека поддерживает все стандартные методы HTTP и может легко обрабатывать несколько одновременных запросов, а также предоставляет расширенные возможности: кэширование запросов/ответов, объединение подключений в пул (connection pooling), аутентификация и др.

О том, почему иногда стоит использовать OkHttp, а не вездесущий Retrofit, можно посмотреть в видео от Android Broadcast. Краткое пояснение дано в следующем пункте статьи.

В статье подробно рассмотрены основные классы и методы библиотеки и представлены основы работы с ней в Android‑разработке.

Содержание:

  • Преимущества OkHttp

  • Основные классы и методы

  • Простой GET‑запрос (синхронный/асинхронный)

  • Сериализация/десериализация

  • Простой POST‑запрос

  • Особенности работы с HTTPS

  • Аутентификация на сервере

  • Использование вместе с ViewModel

Преимущества OkHttp

OkHttp — это библиотека более низкого уровня, чем Retrofit. Это означает, что HTTP‑запросы, автоматизированные в Retrofit с помощью аннотаций, придётся писать вручную. Однако в этом и главный плюс библиотеки: она предоставляет более обширный функционал и настройки соединения, что может повысить производительность и сократить использование памяти. К слову, Retrofit под капотом использует OkHttp.

Библиотека разработана как легкая и эффективная, с акцентом на снижение задержек и повышение работоспособности. Это достигается за счет применения различных методов оптимизации, таких как повторное использование соединений, сжатие и конвейеризация.

Преимущества OkHttp:

  • Гибкость: Библиотека предоставляет больше контроля над процессом сетевого взаимодействием за счёт дополнительных функций, например, пользовательской обработки запросов и ответов.

  • Лёгкость: OkHttp — более компактная библиотека, чем Retrofit, что позволяет минимизировать размер используемой приложением памяти.

  • Кэширование: Библиотека имеет встроенную поддержку HTTP‑кэширования, что может повысить производительность и снизить нагрузку на сеть.

  • Аутентификация: OkHttp предоставляет гибкий и расширяемый API аутентификации, что упрощает реализацию различных её моделей.

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

  • WebSockets: OkHttp обеспечивает встроенную поддержку WebSockets, что позволяет легко реализовать коммуникацию с сервером в режиме реального времени.

Основные классы и методы

1) Настройка клиента и запроса

Класс OkHttpClient — клиент для HTTP-вызовов, который можно использовать для отправки запросов и чтения ответов.

OkHttpClient.Builder — класс предоставляющий методы для настройки клиента, например кэш, аутентификация, перехватчики, тайм-ауты и др. По завершению настройки используется метод build(), который возвращает экземпляр класса OkHttpClient.

OkHttp работает лучше при создании одного экземпляра OkHttpClient и повторном его использовании для всех HTTP-вызовов. Так происходит потому, что каждый клиент содержит свой собственный пул соединений и пул потоков. Повторное использование соединений и потоков уменьшает задержку и экономит память. И наоборот, создание клиента для каждого запроса приводит к трате ресурсов на незадействованные пулы.

Класс Request представляет собой HTTP-запрос. Request.Builder позволяет установить параметры запроса, например url и заголовки.

В целом, HTTP-заголовки представляют собой что-то похожее на Map<String, String>: каждое поле имеет одно значение или не имеет его вовсе. Однако некоторые заголовки могут иметь несколько значений. В связи с этим для добавления заголовка к запросу применяются два метода:

  • header(name, value) — устанавливает только одно значение заголовка name. При этом все существующие значения заголовка будут удалены, и после этого будет установлено новое значение.

  • addHeader(name, value) — добавляет заголовок без удаления уже имеющихся значений.

При чтении заголовка из ответа используйте header(name), чтобы вернуть последнее вхождение заголовка (зачастую это единственное вхождение). Если значение отсутствует, header(name) вернет null. Чтобы прочитать все значения заголовка в виде списка, используйте headers(name).

Для установки целевого URL-адреса запроса используется метод url(). По завершению настройки запроса используется метод build(), который возвращает объект Request.

2) Отправка запроса

newCall — метод класса OkHttpClient, который подготавливает запрос к выполнению в будущем. Принимает объект Request и возвращает объект Call.

Класс Call (вызов) — это запрос, который был подготовлен к выполнению. Вызов может быть отменен. Поскольку экземпляр класса представляет одну пару запрос/ответ, он не может быть выполнен дважды. Для выполнения запроса существуют два метода:

  • execute() — при синхронном вызове. Метод незамедлительно выполняет запрос и блокирует поток до тех пор, пока ответ не будет доступен для обработки или пока не возникнет ошибка.

  • enqueue() — при асинхронном вызове. Метод назначает запрос на выполнение в определенный момент в будущем. Диспетчер определяет, когда будет выполнен запрос: обычно сразу же, если в данный момент не выполняются несколько других запросов. Позже клиент получает объект responseCallback либо с HTTP-ответом, либо с исключением в случае возникновения ошибки.

3) Чтение ответа

Класс Response представляет HTTP-ответ. Тело ответа — свойство экземпляра класса, которое может быть использовано только один раз и затем закрыто. Все остальные свойства неизменяемы.

Прежде чем как-либо использовать тело ответа, необходимо проверить, был ли запрос к серверу успешен. Для этого существует метод isSuccessful() вышеупомянутого класса. Метод проверяет код состояния (status code) HTTP-ответа и возвращает значение true, если код находится в диапазоне 200-300. Если код находится за пределами этого диапазона, он возвращает значение false, указывающее, что запрос не был успешным.

Неуспешный запрос означает, что возникли проблемы на стороне клиента или сервера. Например, запрос был неправильно составлен, на сервере произошла ошибка или сервер некорректно обработал запрос. Если не проверять код состояния, то в конечном счёте можно работать с ответом, который не содержит ожидаемых данных.

Для получения тела ответа используется метод body() класса Response, который возвращает экземпляр класса ResponseBody.

ResponseBody — одноразовый поток от сервера к клиенту, содержащий тело ответа в виде необработанных байтов. Каждое тело ответа поддерживается активным подключением к веб-серверу.

Класс ResponseBody поддерживает потоковую передачу очень больших ответов. Например, его можно использовать для чтения ответа, размер которого превышает всю память, выделенную текущему процессу. Можно даже передавать в потоковом режиме ответ, объем которого превышает общий объем памяти на текущем устройстве, что является обычным требованием для стриминговых видео-приложений.

Класс не загружает весь ответ в память, поэтому тело ответа может быть считано только один раз. Для этого существует несколько методов:

  • bytes() и string() — считывают весь текст ответа в память, а затем возвращают его в виде массива байтов или строки соответственно. Методы следует использовать только для небольших ответов. При считывании больших ответов будет вызвана ошибка OutOfMemoryError.

  • source, byteStream, charStream — предназначены для потокового чтения ответа. Метод source возвращает объект BufferedSource, позволяющий читать тело ответа в виде потока байтов. byteStream работает аналогично, но возвращает объект InputStream. charStream — возвращает объект Reader, который позволяет читать тело ответа в виде потока символов.

Если использовать body() без упомянутых методов, то будет получен сам объект ResponseBody, с которым ничего особо не поделаешь.

Простой GET-запрос (синхронный/асинхронный)

Перед использованием библиотеки нужно добавить соответствующую зависимость в Gradle:

implementation 'com.squareup.okhttp3:okhttp:4.10.0'

Номер последней версии можно посмотреть на Maven Central.

Синхронный запрос (Java):

OkHttpClient client = new OkHttpClient();

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("Запрос к серверу не был успешен: " +
                response.code() + " " + response.message());
    }
    // пример получения конкретного заголовка ответа
    System.out.println("Server: " + response.header("Server"));
    // вывод тела ответа
    System.out.println(response.body().string());
} catch (IOException e) {
    System.out.println("Ошибка подключения: " + e);
}

Синхронный запрос (Kotlin):

val client = OkHttpClient()

val request = Request.Builder()
    .url("https://publicobject.com/helloworld.txt")
    .build()

try {
    client.newCall(request).execute().use { response ->
        if (!response.isSuccessful) {
            throw IOException("Запрос к серверу не был успешен:" +
                    " ${response.code} ${response.message}")
        }
        // пример получения конкретного заголовка ответа
        println("Server: ${response.header("Server")}")
        // вывод тела ответа
        println(response.body!!.string())
    }
} catch (e: IOException) {
    println("Ошибка подключения: $e");
}

В то время как в Java используются методы объектов, в Kotlin иногда используются их свойства. Например, свойство body объекта Response.

Каждое тело ответа поддерживается ограниченным ресурсом. Поэтому после использования оно должно быть закрыто. Закрытие ресурса освобождает все системные средства, которые были выделены ресурсу, и делает его доступным для сбора мусора (garbage collection). Если не закрыть тело ответа, произойдет утечка ресурсов, что в конечном итоге может привести к замедлению или крашу приложения.

Для закрытия ресурса можно использовать метод close(), но предпочтительнее использовать блок try-with-resources (Java) и метод use (Kotlin). Обе конструкции выполняют блок кода относительно заданного ресурса, а затем корректно закрывают его, независимо от того, вызвано исключение или нет.

Асинхронный запрос (Java):

OkHttpClient client = new OkHttpClient();

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("Запрос к серверу не был успешен: " +
                        response.code() + " " + response.message());
            }

            // пример получения всех заголовков ответа
            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());
        }
    }
});

Асинхронный запрос (Kotlin):

val client = OkHttpClient()

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("Запрос к серверу не был успешен:" +
                        " ${response.code} ${response.message}")
            }
            // пример получения всех заголовков ответа
            for ((name, value) in response.headers) {
                println("$name: $value")
            }
            // вывод тела ответа
            println(response.body!!.string())
        }
    }
})

Асинхронный запрос выполняется в потоке Worker. Когда ответ доступен для чтения выполняется обратный вызов (сallback). Этот вызов выполнится после того, как будут готовы заголовки ответа. Чтение тела ответа все еще может блокировать поток. OkHttp в настоящее время не предлагает асинхронных API для получения тела ответа по частям.

Callback имеет два абстрактных метода:

  • onResponse — вызывается, когда HTTP-ответ был успешно получен от удаленного сервера.

  • onFailure — вызывается, когда запрос не может быть выполнен из-за проблем с подключением, тайм-аута или при его отмене. Поскольку в сети может произойти сбой во время соединения с сервером, возможен случай, когда удаленный сервер успевает принять запрос до сбоя.

Сериализация/десериализация

В данном пункте кратко рассмотрена сериализация и десериализация объектов (их преобразование в определённую последовательность байтов, которую можно передать по сети, и наоборот).

Для того, чтобы преобразовать объект в строку JSON или наоборот можно воспользоваться библиотеками Gson и/или Moshi.

Вкратце, если вам нужна проста использования и широкий набор функций, то выбираете Gson. Если нужна производительность и эффективное использование памяти, то лучшим выбором будет Moshi.

Рассмотрим пример сериализации с помощью Moshi (Java):

// Создание объекта Moshi
Moshi moshi = new Moshi.Builder().build();
// Создание адаптера
JsonAdapter<SomeClass> jsonAdapterRequest =
        moshi.adapter(SomeClass.class);
// Сериализация, SomeClassInstance - экземляр класса SomeClass
String jsonRequest = jsonAdapterRequest.toJson(SomeClassInstance);

То же самое в Kotlin:

// Создание объекта Moshi,
// для Kotlin необходимо добавлять KotlinJsonAdapterFactory
val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory()).build()
// Создание адаптера
val jsonAdapterRequest = moshi.adapter(SomeClass::class.java)
// Сериализация, SomeClassInstance - экземляр класса SomeClass
val jsonRequest = jsonAdapterRequest.toJson(SomeClassInstance)

Для сериализации необходимо создать объект Moshi, адаптер и передать ему тип сериализуемого объекта. В данном случае это тип Class.

Если требуется сериализовать более сложный объект, например коллекцию, то тип можно передать двумя способами.

1) С помощью метода Types.newParameterizedType(), который создает новый параметризованный тип.

// Java
JsonAdapter<List<SomeClass>> jsonAdapterRequest = moshi.adapter(
        Types.newParameterizedType(List.class, SomeClass.class)
);
// Kotlin
val jsonAdapterRequest = moshi.adapter<List<SomeClass>>(
    Types.newParameterizedType(List::class.java, SomeClass::class.java)

2) С помощью класса TypeToken библиотеки Gson. Класс используется для передачи информации о типах во время выполнения программы. Конструктор класса возвращает представленный класс из заданного типа.

// Java
JsonAdapter<List<SomeClass>> jsonAdapterRequest =
        moshi.adapter(new TypeToken<List<SomeClass>>(){}.getType());
// Kotlin
val jsonAdapterRequest = moshi.adapter<List<SomeClass>>(
    object : TypeToken<List<SomeClass?>?>(){}.type
)

Разница способов состоит в том, что TypeToken более типобезопасен (typesafe), а Types.newParameterizedType более эффективен.

Десериализация осуществляется аналогичным образом.

// Java

// Создание адаптера для десериализации
// Используется тот же объект Moshi, что и при сериализации
JsonAdapter<SomeClass> jsonAdapterResponse = 
        moshi.adapter(SomeClass.class);
// Десериализация
String jsonResponse = jsonAdapterResponse.fromJson(receivedData);
// Kotlin

// Создание адаптера для десериализации
// Используется тот же объект Moshi, что и при сериализации
val jsonAdapterResponse = moshi.adapter(SomeClass::class.java)
// Десериализация
val jsonResponse = jsonAdapterResponse.fromJson(receivedData)

При сериализации/десериализации Moshi может вызывать разного рода исключения, к примеру если десериализуемая строка не является строкой JSON или если строка не соответствует объекту, в который её пытаются преобразовать.

Если серверная и клиентская часть настроены правильно, то такого не должно происходить. Но всё же рекомендуется оборачивать операции Moshi в блок try-catch.

Простой POST-запрос

Чтобы сделать POST-запрос, используется метод post() класса Request.Builder. Метод принимает RequestBody, который он добавляет к запросу.

POST-запрос в Java:

MediaType JSON = MediaType.get("application/json; charset=utf-8");
String jsonRequest = "Some request";

OkHttpClient client = new OkHttpClient();
RequestBody body = RequestBody.create(jsonRequest, JSON);
Request.Builder requestBuilder = new Request.Builder().url(serverUrl).post(body);
Request request = requestBuilder.build();

try (Response response = client.newCall(request).execute()) {
    if (!response.isSuccessful()) {
        throw new IOException("Запрос к серверу не был успешен: " +
                response.code() + " " + response.message());
    }
    System.out.println(response.body().string());
} catch (IOException e) {
    System.out.println("Ошибка подключения: " + e);
}

POST-запрос в Kotlin:

val jsonRequest = "some request"
val JSON = "application/json; charset=utf-8".toMediaType()

val client = OkHttpClient()
val body: RequestBody = jsonRequest.toRequestBody(JSON)
val request = Request.Builder().url(serverUrl).post(body).build()

try {
    client.newCall(request).execute().use { response ->
        if (!response.isSuccessful) {
            throw IOException("Запрос к серверу не был успешен:" +
                    " ${response.code} ${response.message}")
        }
        println(response.body!!.string())
    }
} catch (e: IOException) {
    println("Ошибка подключения: $e")
}

Объект MediaType необходим для описания типа содержимого тела запроса или ответа. Обычно он используется для установки заголовка «Content-Type» в HTTP-запросе.

Чтобы получить объект MediaType можно использовать один из статических методов одноименного класса:

  • MediaType.parse(String) — создает новый экземпляр MediaType с указанным типом содержимого и кодировкой. Функция возвращает медиатип для строки, или null, если строка не является правильно сформированным медиатипом.

  • MediaType.get(String) — работает аналогично MediaType.parse, но если строка сформирована неправильно, то вызывает исключение IllegalArgumentException.

В Kotlin используется метод toMediaType() объекта String. Метод является аналогом MediaType.get(String).

RequestBody — класс, представляющий собой тело запроса. Экземпляр класса создаётся с помощью метода create.

RequestBody.create(MediaType, String) создает тело запроса с указанным содержимым и его типом. Метод имеет несколько реализаций. Содержимое можно передать в виде массива байтов, файла, строки или объекта okio.ByteString. Тип содержимого всегда указывается с помощью объекта MediaType. Этот объект также устанавливает заголовку «Content-type» соответствующее значение, поэтому вручную устанавливать этот заголовок не нужно.

Аналогом RequestBody.create(MediaType, String) в Kotlin является метод toRequestBody(MediaType?) объекта String.

Особенности работы с HTTPS

OkHttp пытается балансировать между двумя задачами:

  • Подключение к максимально возможному количеству хостов. Сюда входят как современные хосты, на которых используются последние версии boringssl, так и немного устаревшие хосты, на которых используются старые версии OpenSSL.

  • Безопасность соединения. Сюда входит проверка удаленного веб-сервера с помощью сертификатов и конфиденциальность данных, передаваемых с помощью надежных шифров.

При согласовании соединения с HTTPS-сервером OkHttp должен знать, какие предлагать версии TLS и наборы шифров. Для клиента, который хочет максимизировать возможность соединения с различными серверами, это будут устаревшие версии TLS и слабые по конструкции наборы шифров. Для клиента, который хочет максимизировать безопасность, это будут только последняя версия TLS и самые сильные наборы шифров.

Конкретные решения по безопасности и соединению реализуются с помощью ConnectionSpec. OkHttp включает четыре встроенных типа соединений:

  • RESTRICTED_TLS — безопасная конфигурация, предназначенная для удовлетворения более строгих требований по соответствию.

  • MODERN_TLS — безопасная конфигурация, позволяющая подключаться к современным HTTPS-серверам.

  • COMPATIBLE_TLS — безопасная конфигурация, которая подключается к безопасным, но менее современным серверам HTTPS.

  • CLEARTEXT — небезопасная конфигурация, которая используется для URL-адресов http://.

По умолчанию OkHttp будет пытаться установить соединение MODERN_TLS. Если соединение MODERN_TLS не удастся, okhttp3 переключится на другой тип соединения. Точный механизм отката зависит от конкретной реализации okhttp3 и конфигурации, установленной разработчиками.

Настроить конфигурацию можно следующим образом:

OkHttpClient client = new OkHttpClient.Builder()
    .connectionSpecs(Arrays.asList(
        ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
    .build()

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

Аутентификация на сервере

Аутентификацию на сервере можно реализовать двумя способами.

1) Вручную добавить заголовок аутентификации. Полезно в случае, если нужна аутентификация только для одного запроса. Для того, чтобы добавлять заголовок ко всем запросам клиента, можно создать перехватчик. Способ полезен, если у вас статический ключ API или токен, который нужно отправлять с каждым запросом.

// Kotlin

val client = OkHttpClient().newBuilder().addInterceptor { chain ->
        val originalRequest = chain.request()

        val builder = originalRequest.newBuilder()
               .header("Authorization", Credentials.basic("username", "password"))
        val newRequest = builder.build()
        chain.proceed(newRequest)
    }.build()

2) Использовать интерфейс Authenticator — полезно, когда необходимо динамически аутентифицироваться или нужна дополнительная настройка процесса аутентификации.

Интерфейс позволяет выполнить либо предварительную аутентификацию перед подключением к серверу, либо реактивную аутентификацию после получения ответа от веб-сервера или прокси-сервера.

Рассмотрим пример реактивной аутентификации. В таком случае, если код состояния ответа равен 401 (Unauthorized), OkHttp посылает повторный запрос, включающий заголовок «Authorization».

При этом важно сделать проверку, была ли в первоначальном запросе попытка аутентификации. Если да, то, скорее всего, дальнейшие попытки будут бесполезны, и аутентификатор должен отказаться от них.

// Java

OkHttpClient.Builder client = new OkHttpClient.Builder();
client.authenticator((route, response) -> {
    if (response.request().header("Authorization") != null) {
        return null; // Остановить попытки аутентификации,
        // т.к. у нас уже не получилось это сделать
    }
    String credential = Credentials.basic("name", "password");
    return response.request().newBuilder()
            .header("Authorization", credential).build();
});

Здесь метод authenticator с помощью лямбда-функции устанавливает экземпляр интерфейса Authenticator, который предоставляет механизм для проверки ответа от сервера и возвращает запрос, включающий в себя учетные данные клиента. Метод Credentials.basic используется для кодирования имени пользователя и пароля при базовой аутентификации.

Использование вместе с ViewModel

Простой асинхронный запрос в ViewModel можно сделать следующим образом.

// MainViewModel.kt

private val _response = MutableLiveData<String>()
val response: LiveData<String> = _response

private val client = OkHttpClient()

fun getResponseFromServer() {

    val jsonRequest = "your request body"

    val body = jsonRequest.toRequestBody(JSON)
    val request = Request.Builder().url(BASE_URL).post(body).build()

    client.newCall(request).enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
            Log.d(TAG, "Ошибка подключения: $e")
        }

        override fun onResponse(call: Call, response: Response) {
            response.use {
                if (!response.isSuccessful) {
                    throw Exception("Запрос к серверу не был успешен:" +
                            " ${response.code} ${response.message}")
                }
                _response.postValue(response.body!!.string())
            }
        }
    })
}

Метод postValue передаёт задачу по установке значения главному потоку. Если попытаться присвоить значение напрямую, то будет вызвано исключение java.lang.IllegalStateException: Cannot invoke setValue on a background thread.

Необходимо делать именно асинхронный запрос, чтобы не блокировать поток интерфейса и чтобы приложение оставалось отзывчивым. Либо можно самостоятельно настроить синхронный вызов в другом потоке.

Начиная с SDK 10 при попытке синхронного вызова в главном потоке будет вызвано исключение android.os.NetworkOnMainThreadException.

Чтобы сделать запрос в отдельном api-файле и передать ответ переменной из ViewModel можно воспользоваться механизмом callback.

// SomeApiService.kt

interface RequestCallback {
    fun onSuccess(response: String)
    fun onFailure(error: String)
}

class SomeApiService () {

    private val client = OkHttpClient()

    fun makeRequest(callback: RequestCallback) {

        val jsonRequest = "your request body"

        val body = jsonRequest.toRequestBody(JSON)
        val request = Request.Builder().url(BASE_URL).post(body).build()

        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                callback.onFailure(e.toString());
            }

            override fun onResponse(call: Call, response: Response) {
                response.use {
                    if (!response.isSuccessful) {
                        throw Exception("Запрос к серверу не был успешен:" +
                                " ${response.code} ${response.message}")
                    }
                    callback.onSuccess(response.body!!.string())
                }
            }
        })
    }
}

object SomeApi {
    val someService : SomeApiService by lazy {
        SomeApiService()
    }
}
// MainViewModel.kt

private val _response = MutableLiveData<String>()
val response: LiveData<String> = _response

private val client = OkHttpClient()

fun getResponseFromApi() {
    SomeApi.someService.makeRequest(object : RequestCallback {
        override fun onSuccess(response: String) {
            _response.postValue(response)
        }

        override fun onFailure(error: String) {
            Log.d(TAG, "Ошибка подключения: $error")
        }
    })
}

В файле SomeApiService.kt находится интерфейс RequestCallback, класс SomeApiService с методом makeRequest(), который делает запрос к серверу, и объект SomeApi, через который будет осуществляться доступ к экземпляру класса.

В MainViewModel.kt функция getResponseFromApi() реализует интерфейс RequestCallback и передает его в качестве параметра методу makeRequest().

Тело функции getResponseFromApi() можно обернуть во viewModelScope.launch {…}, чтобы запрос был отменён при очистке (разрушении) MainViewModel.

Заключение

OkHttp — гибкая библиотека, выступающая в роли HTTP-клиента.

В отличии Retrofit настраивать клиент, писать запросы и обрабатывать ответы необходимо вручную. Это одновременно и преимущество и недостаток OkHttp. Недостаток заключается в необходимости писать много шаблонного кода. Преимущество — возможность кастомизировать соединение.

Из-за слабой кастомизируемости в некоторых случаях Retrofit может не подойти, и без OkHttp не обойтись. Также благодаря кастомизации OkHttp можно повысить производительность и уменьшить использование памяти.

Полезные ресурсы: Подробнее про Authenticator; Различные примеры использования библиотеки; Перехватчики (Interceptors).

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.

Понравилась статья? Поделить с друзьями:
  • Обоюдная ошибка или переигровка жест
  • Обоюдная ошибка и переигровка волейбол
  • Обоюдная ошибка и переигровка в волейболе
  • Обоюдная ошибка в волейболе что это
  • Обои подруги были удивлены неожиданному появлению сергея где ошибка