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

в 17:05, , рубрики: android, kotlin, Разработка под android

Начнём с постановки задачи.

  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 прерыватель.

Автор: sin28

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js