Как я сделал кастомный прерыватель Okhttp через котлиновские корутины

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Начнём с постановки задачи.

  1. Надо в каждом запросе в header’s отправлять токен и id юзера
  2. Надо из каждого ответа вытаскивать из headers новый токен и id юзера
  3. Полученные данные надо сохранять

Библиотека для серверного взаимодействия – Retrofit. За многопоточность отвечают корутины.
Задача не сложная, надо просто добавить прерыватель Okhttp client в каждый запрос. Полчаса и всё готово, всё работает, все рады. Но мне стало интересно, а нельзя ли сделать прерыватель без Okhttp клиента?

Начнём решать задачи по порядку. Если с добавлением header нет проблем (надо только в запрос добавить @HeaderMap), то как получить headers которые приходят в ответе? Очень просто, надо наш ответ обернуть в класс Response, у которого есть метод headers().

Вот такой был интерфейс запросов:

@FormUrlEncoded
@POST("someurl/")
suspend fun request1(@Field("idLast") idLastFeed: Long,
                     @Field("autoview") autoView: Boolean,
                     @HeaderMap headers: Map<String, String?>): Answer1
@FormUrlEncoded
@POST("someurl/")
suspend fun request2(@Field("ransom") ransom: Long,
                                @HeaderMap headers: Map<String, String?>): Answer2

А вот такой стал:

@FormUrlEncoded
@POST("someurl")
suspend fun request1(@Field("idLast") idLastFeed: Long,
                     @Field("autoview") autoView: Boolean,
                     @HeaderMap headers: Map<String, String?>?): Response<Answer1>

@FormUrlEncoded
@POST("someurl")
suspend fun request2(@Field("ransom") ransom: Long,
                  @HeaderMap headers: Map<String, String?>?): Response<Answer2>

Теперь для каждого запроса надо добавлять параметр headersMap. Создадим отдельный класс RestClient для оболочки запросов, чтобы постоянно в презентере не вытаскивать из sharedPreferences токен и id. Вот так получается:

class RestClient(private val api: Api, private val prefs: SharedPreferences) {

    suspend fun request1(last: Long, autoView: Boolean): Answer1 {
        return api.request1(last, autoView, headers())
    }

    suspend fun request2(id: Long): Answer2 {
        return api.request2(id, headers())
    }
    private val TOKEN_KEY = "Token"
    private val ID_KEY = "ID"
    fun headers(): Map<String, String> {
        return mapOf(
            TOKEN_KEY to prefs.getString(Constants.Preferences.SP_TOKEN_KEY, ""),
            ID_KEY to prefs.getLong(Constants.Preferences.SP_ID, -1).toString()
        )
    }
}

Видно, что мы делаем одно и тоже:

  1. Получаем какие-то параметры для запроса.
  2. Добавляем к запросу headers.
  3. Вызываем метод.
  4. Вытаскиваем новые значения из headers.
  5. Возвращаем результат.

Почему бы нам не сделать одну функцию для всех запросов? Для этого изменим запросы. Вместо отдельных переменных с типом @Field, теперь мы будем использовать @FieldMap. Это будет первый параметр для нашей функции – перывателя. Вторым параметром у нас будет сам запрос. Здесь я использовал Kotlin DSL (мне так захотелось). Я создал класс Request, в котором сделал функцию send для вызова запроса.

Вот так выглядит интерфейс запросов:

@FormUrlEncoded
@POST("someurl/")
suspend fun feedListMap(@FieldMap map: HashMap<String, out Any>?,
            @HeaderMap headers: Map<String, String?>?): Response<Answer1>

@FormUrlEncoded
@POST("someurl/")
suspend fun feedListMap(@FieldMap map: HashMap<String, out Any>?,
             @HeaderMap headers: Map<String, String?>?): Response<Answer2>

А вот так выглядит класс Request:

class Request<T>(
    var fieldHashMap: java.util.HashMap<String, out Any> = hashMapOf(),
    var headersHashMap: Map<String, String?>? = mapOf(),
    var req: suspend (HashMap<String, out Any>?, Map<String, String?>?) -> Response<T>? = { _,_ -> null}
){ 
    fun send(): Response<T>? {
        return runBlocking {
            try {
                req.invoke(fieldHashMap, headersHashMap)
            } catch (e: Exception) {
                throw Exception(e.message ?: "Ошибка запроса")
            } catch (t: Throwable) {
                throw Exception(t.message ?: "Ошибка запроса")
            }
        }
    }
}

Теперь же класс RestClient выглядит так:

class RestClient(private val api: Api, private val prefs: SharedPreferences) {

    private val TOKEN_KEY = "Token"
    private val ID_KEY = "ID"
    fun headers(): Map<String, String> {
        return mapOf(
            TOKEN_KEY to prefs.getString(Constants.Preferences.SP_TOKEN_KEY, ""),
            ID_KEY to prefs.getLong(Constants.Preferences.SP_ID, -1).toString()
        )
    }

    fun <T> buildRequest(request: Request<T>.() -> Unit): T? {
        val req = Request<T>()
        request(req)
        val res = req.send()
        val newToken = res?.headers()?.get(TOKEN_KEY)
        val newID = res?.headers()?.get(ID_KEY)?.toLong()
        if (newToken.notNull() && newID.notNull()) {
            prefs.edit()
                .putString(TOKEN_KEY, newToken)
                .putLong(ID_KEY, newID)
                .apply()
        }
        return res?.body()
    }

    fun fiedsMapForRequest1(last: Long, autoView: Boolean) = hashMapOf("idLast" to last, "autoview" to autoView)

    fun fiedsMapForRequest2(ransom: Long, autoView: Boolean) = hashMapOf("ransom" to ransom)

}

И, наконец, вот так мы в презентере вызываем наши запросы:

try {
            val answer1 = restClient.buildRequest<Answer1> {
                fieldHashMap = restClient.fiedsMapForRequest1(1, false)
                headersHashMap = restClient.headers()
                req = api::request1
            }
           val answer2 = restClient.buildRequest<Answer2> {
                fieldHashMap = restClient.fiedsMapForRequest2(1234)
                headersHashMap = restClient.headers()
                req = api::request2
            }
            // do something with answer
  } catch (e: Exception) {
           viewState.showError(e.message.toString())
  } catch (e: InterruptedException) {
            viewState.showError(e.message.toString())
  } finally {
            viewState.hideProgress()
  }

Вот такой я сделал с помощью котлина кастомный прерыватель.

P.S. Решение этой задачи было очень увлекательно, но, к сожалению, в проекте используется Okhttp прерыватель.
Источник: https://habr.com/ru/post/465781/


Интересные статьи

Интересные статьи

Наверняка многие ведут учет сделок и следят за состоянием своего портфеля в Google-таблицах или в Excel. Раньше мне приходилось вручную вносить информацию о каждом купленном или прода...
Продолжаю публикацию решений отправленных на дорешивание машин с площадки HackTheBox. В данной статье эксплуатируем XXE в сервисе преобразования DOCX документов в PDF, получаем ...
Всем привет. В этой статье расскажу о том, как мне удалось реализовать управление Arduino через интернет с помощью подключенного к интернету ПК. В общем случае данный способ можно использовать дл...
Ранее считалось, что после остановки сердца или прекращении активности мозга по другим причинам, без кислорода и электрической активности клетки мозга начинают умирать через несколько минут, и пр...
«Матрица» — фильм братьев сестёр Вачовски — насыщен смыслами: философскими, религиозными и культурными, а иногда в нем находят теории заговора. Есть еще один смысл — командный. В команде есть мат...