moko-network
moko-network
Библиотека moko-network - позволяет генерировать сущности и API классы из OpenAPI (Swagger) файлов.
moko-network-errors
Библиотека moko-network содержит внутри себя модуль moko-network-errors - интеграцию с moko-errors, чтобы удобно обрабатывать ошибки сети.
Для начала, ознакомьтесь с этим модулем по README.
Для использования этого модуля, проделайте следующие действия:
- Подключить его в project.build.gradle:
commonMainApi("dev.icerock.moko:network-errors:$mokoNetworkVersion") - Вызвать метод для регистрации основных ошибок сети:
ExceptionMappersStorage.registerAllNetworkMappers(), его реализацию можете посмотреть тут.- Если вы хотите изменить текст при основных ошибках сети - переопределите параметр
errorsTextsметодаregisterAllNetworkMappers. - Если вы хотите добавить обработку другого класса ошибок, используйте
ExceptionMappersStorage.register- метод изmoko-errors.
- Если вы хотите изменить текст при основных ошибках сети - переопределите параметр
- Используйте
exceptionHandlerдля автоматической обработки ошибок сети:viewModelScope.launch {
exceptionHandler.handle {
api.mareTestRequest()
// ...
}.execute()
}
Features
Библиотека moko-network содержит в себе фичи - классы, реализующие интерфейс HttpClientFeature из Ktor.
В версии Ktor 2.0.0 интерфейс HttpClientFeature переименовали в HttpClientPlugin.
Ktor содержит уже готовые плагины, вот, например, для чего их можно использовать:
- Cache - включить кеширование для каждого запроса, чтобы они отрабатывали быстрее
- DefaultRequest добавлять для всех запросов какие-нибудь хидеры по умолчанию
- Logging - плагин для логгирования запросов
- BodyProgress - плагин для получения
observableпрогресса загрузки и скачивания - HttpTimeout - плагин для настройки таймаутов
С полным списком плагинов, доступных в Ktor вы можете ознакомиться по ссылке.
Также, примеры использования стандартных плагинов можно посмотреть в статье Kotlin Multiplatform Mobile: Intercepting Network Request and Response.
Плагин выполняет свою задачу, как правило, для каждого запроса или ответа сервера. Чтобы понять, что делает каждый плагин, смотрите реализацию метода
handle, для плагинов из Ktorinstallвcompanion object, для плагинов изmoko-network
Подключение
Пример создания httpClient, в котором происходит подключение и настройка плагинов.
Теперь рассмотрим те плагины, которые есть в moko-network.
ExceptionFeature
Эта фича просто кидает ошибку, если status ответа сервера неудачный.
override fun install(feature: ExceptionFeature, scope: HttpClient) {
scope.responsePipeline.intercept(HttpResponsePipeline.Receive) { (_, body) ->
if (body !is ByteReadChannel) return@intercept
val response = context.response
if (!response.status.isSuccess()) {
val packet = body.readRemaining()
val responseString = packet.readText(charset = Charset.forName("UTF-8"))
throw feature.exceptionFactory.createException(
request = context.request,
response = context.response,
responseBody = responseString
)
}
proceedWith(subject)
}
}
LanguageFeature
Эта фича позволяет добавить язык к каждому запросу, чтобы уведомить сервер, на каком языке мы хотим получить ответ.
override fun install(feature: LanguageFeature, scope: HttpClient) {
scope.requestPipeline.intercept(HttpRequestPipeline.State) {
feature.languageProvider.getLanguageCode()?.apply {
context.header(feature.languageHeaderName, this)
}
}
}
TokenFeature
Эта фича к каждому запросу добавляет токен, например для авторизации, по ключу, которое вы укажите в tokenHeaderName, при настройке фичи. (обычно - authorization)
Для использования фичи необходимо реализовать метод получения токена - getToken().
override fun install(feature: TokenFeature, scope: HttpClient) {
scope.requestPipeline.intercept(HttpRequestPipeline.State) {
feature.tokenProvider.getToken()?.apply {
context.headers.remove(feature.tokenHeaderName)
context.header(feature.tokenHeaderName, this)
}
}
}
RefreshTokenFeature
Бывают ситуации, когда у токена есть время жизни, по истечении которого токен становится недействителен. В этом случае необходимо как-то его обновить.
За правила обновления и сохранения нового токена отвечает метод feature.updateTokenHandler.invoke().
Этот блок кода отвечает за создание реквеста, подставления туда текущего использующегося токена и выполнения запроса.
val requestBuilder = HttpRequestBuilder().takeFrom(context.request)
val result: HttpResponse = context.client!!.request(requestBuilder)
proceedWith(result)
Что может произойти?
Отправили запрос - получили ответ - 401 ошибка авторизации. После этого мы обновили токен и повторили запрос - все ок.
Но, может получиться так, что мы успели отправить несколько запросов с неправильным токеном, и каждому из них придет ответ - ошибка авторизации.
Первое, что нам нужно сделать в этом случае - проверить, отличается ли тот токен, который мы отправили от того, который находится у нас в хранилище. За эту проверку отвечает метод feature.isCredentialsActual().
Если токены отличаются, значит какой-то запрос до нас его уже обновил, и нам нужно просто повторно отправить наш запрос, но уже с новым токеном.
if (!feature.isCredentialsActual(context.request)) {
refreshTokenHttpFeatureMutex.unlock()
val requestBuilder = HttpRequestBuilder().takeFrom(context.request)
val result: HttpResponse = context.client!!.request(requestBuilder)
proceedWith(result)
return@intercept
}
В случае, если мы получили ошибку 401, но токен который мы отправили не отличается от того, который находится у нас в хранилище - просто обновляем токен методом feature.updateTokenHandler.invoke().
Если обновление токена прошло успешно - повторяем запрос с новым токеном. Если обновить не удалось - отправляем результат дальше, чтобы показать проблему юзеру.
if (feature.updateTokenHandler.invoke()) {
// Если обновление токена прошел успешно, пробуем повторить запрос
refreshTokenHttpFeatureMutex.unlock()
val requestBuilder = HttpRequestBuilder().takeFrom(context.request)
val result: HttpResponse = context.client!!.request(requestBuilder)
proceedWith(result)
} else {
// Если не удалось обновить токен -
refreshTokenHttpFeatureMutex.unlock()
proceedWith(subject)
}