Skip to main content

moko-network

moko-network

Библиотека moko-network - позволяет генерировать сущности и API классы из OpenAPI (Swagger) файлов.



moko-network-errors

Библиотека moko-network содержит внутри себя модуль moko-network-errors - интеграцию с moko-errors, чтобы удобно обрабатывать ошибки сети. Для начала, ознакомьтесь с этим модулем по README.

Для использования этого модуля, проделайте следующие действия:

  1. Подключить его в project.build.gradle: commonMainApi("dev.icerock.moko:network-errors:$mokoNetworkVersion")
  2. Вызвать метод для регистрации основных ошибок сети: ExceptionMappersStorage.registerAllNetworkMappers(), его реализацию можете посмотреть тут.
    • Если вы хотите изменить текст при основных ошибках сети - переопределите параметр errorsTexts метода registerAllNetworkMappers.
    • Если вы хотите добавить обработку другого класса ошибок, используйте ExceptionMappersStorage.register - метод из moko-errors.
  3. Используйте 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, для плагинов из Ktor
  • install в 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)
}