- Надо в каждом запросе в header’s отправлять токен и id юзера
- Надо из каждого ответа вытаскивать из headers новый токен и id юзера
- Полученные данные надо сохранять
Библиотека для серверного взаимодействия – 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()
)
}
}
Видно, что мы делаем одно и тоже:
- Получаем какие-то параметры для запроса.
- Добавляем к запросу headers.
- Вызываем метод.
- Вытаскиваем новые значения из headers.
- Возвращаем результат.
Почему бы нам не сделать одну функцию для всех запросов? Для этого изменим запросы. Вместо отдельных переменных с типом @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)
androidovshchik
01.09.2019 20:35} catch (e: Exception) {
viewState.showError(e.message.toString())
} catch (e: InterruptedException) {
viewState.showError(e.message.toString())
}
Второй catch никогда не сработаетsin28 Автор
02.09.2019 09:58Согласен, исправил
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 ?: «Ошибка запроса»)
}
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() }
Вот и все, проще и понятнее чем мудреж с дженерикамиsin28 Автор
02.09.2019 10:13Да, так изначально и было. Но в каждой функции класса TokenApi вы делаете одно и тоже. В чём отличие функции request1 от request2? Только в самом запросе. Так почему бы не передавать запрос в параметре, а не делать для всех запросов обёртки?
pin2t
02.09.2019 14:52Так и в варианте с жденериками куча повторов, например
fieldHashMap = restClient.fiedsMapForRequest2(1234) headersHashMap = restClient.headers()
Но это не главное, главное что в buildRequest передается функция которую надо вызвать, и список параметров которые надо передать. Это просто непонятный сложный вызов функции.
Если бы buildRequest возвращал Request и не выполнял его сразу, а например куда-то складывал для последующего выполнения — это имело бы смысл. Но для простого вызова функции так усложнять незачем, по-моему.
midery
01.09.2019 21:52А почему бы просто не задействовать interceptors, и не городить кучу лишнего кода в presenter-слое?
advance
02.09.2019 16:40Тоже так подумал. Единственный минус interceptors для такого функционала в том, что там нужно реализовать синхронизацию в 2х или 3х местах, чтобы не пустить запросы из параллельных потоков до конца процессов установки/обновления токенов. При этом еще не получить deadlock.
Так что в целом interceptors- более правильный вариант, но требует больших знаний о многопоточности
pin2t
Тихий ужас у вас в коде. Почему
возвращает Answer?, логично чтобы buildRequest возвращал Request, не?