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

  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())
  } finally {
            viewState.hideProgress()
  }

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

P.S. Решение этой задачи было очень увлекательно, но, к сожалению, в проекте используется Okhttp прерыватель.

Комментарии (11)


  1. pin2t
    01.09.2019 20:27

    Тихий ужас у вас в коде. Почему

    val answer1 = restClient.buildRequest<Answer1> {
                

    возвращает Answer?, логично чтобы buildRequest возвращал Request, не?


  1. androidovshchik
    01.09.2019 20:35

    } catch (e: Exception) {
    viewState.showError(e.message.toString())
    } catch (e: InterruptedException) {
    viewState.showError(e.message.toString())
    }

    Второй catch никогда не сработает


    1. sin28 Автор
      02.09.2019 09:58

      Согласен, исправил


      1. S-trace
        02.09.2019 18:30

        } catch (e: Exception) {
        throw Exception(e.message ?: «Ошибка запроса»)
        } catch (t: Throwable) {
        throw Exception(t.message ?: «Ошибка запроса»)
        }

        Почему бы не сделать проще:
        } catch (t: Throwable) {
        throw Exception(t.message ?: «Ошибка запроса»)
        }


  1. pin2t
    01.09.2019 20:46

    Такая кажущаяся общей функция на самом деле нифига не общая и не решает никаких задач. вы также туда передаете api::request1 и api::request2 и параметры к ним, просто в очень непонятном и причудливом виде.
    Надо просто сделать класс TokenApi, который будет вызывать эти же функции но добавлять токен

    class TokenApi(private val api: Api, private val prefs: SharedPreferences) {
    
        suspend fun request1(last: Long, autoView: Boolean): Answer1 {
            val res = api.request1(last, autoView, headers())
            saveToken(res)
            return res?.body()
        }
    
        suspend fun request2(id: Long): Answer2 {
            val res =  api.request2(id, headers())
            saveToken(res)
            return res?.body()
        }
        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 saveToken(res) {
            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()
            }
        }
    }
    
    ...
    
    
    try {
        val tApi = new TokenApi(api, prefs)
        val answer1 = tApi.request1(1, false)
        val answer2 = tApi.request2(1234)
    } catch (e: Exception) {
           viewState.showError(e.message.toString())
    } catch (e: InterruptedException) {
            viewState.showError(e.message.toString())
    } finally {
            viewState.hideProgress()
    }
    

    Вот и все, проще и понятнее чем мудреж с дженериками


    1. dimka11
      01.09.2019 20:48

      catch нужно поменять местами


      1. pin2t
        01.09.2019 21:04

        Я бы вообще второй catch убрал. Я про общую идею, исключения я просто скопировал. Там и с корутинами лажа вообщем-то


    1. sin28 Автор
      02.09.2019 10:13

      Да, так изначально и было. Но в каждой функции класса TokenApi вы делаете одно и тоже. В чём отличие функции request1 от request2? Только в самом запросе. Так почему бы не передавать запрос в параметре, а не делать для всех запросов обёртки?


      1. pin2t
        02.09.2019 14:52

        Так и в варианте с жденериками куча повторов, например

        fieldHashMap = restClient.fiedsMapForRequest2(1234)
                        headersHashMap = restClient.headers()

        Но это не главное, главное что в buildRequest передается функция которую надо вызвать, и список параметров которые надо передать. Это просто непонятный сложный вызов функции.
        Если бы buildRequest возвращал Request и не выполнял его сразу, а например куда-то складывал для последующего выполнения — это имело бы смысл. Но для простого вызова функции так усложнять незачем, по-моему.


  1. midery
    01.09.2019 21:52

    А почему бы просто не задействовать interceptors, и не городить кучу лишнего кода в presenter-слое?


    1. advance
      02.09.2019 16:40

      Тоже так подумал. Единственный минус interceptors для такого функционала в том, что там нужно реализовать синхронизацию в 2х или 3х местах, чтобы не пустить запросы из параллельных потоков до конца процессов установки/обновления токенов. При этом еще не получить deadlock.
      Так что в целом interceptors- более правильный вариант, но требует больших знаний о многопоточности