Encode String to UTF-8 in Kotlin
But this results in an error, because I believe the Kotlin String() method is different than what I am doing in Java. Is it as simple as using the toString()?
String response = response.data.toString() // kotlin code
How does the system know to use UTF-8, or is that just the default? This is just hypothetical, but what if I wanted to do something with String object and therefore used UTF-16? How can I change the encoding?
Основы OkHttp в Android-разработке
OkHttp — библиотека и по совместительству HTTP‑клиент с открытым исходным кодом для Java и Kotlin, разработанная Square, которая также создала Retrofit.
OkHttp предоставляет простой, легкий в использовании API для выполнения HTTP‑запросов, включая поддержку протоколов HTTP/1.1 и HTTP/2. Библиотека поддерживает все стандартные методы HTTP и может легко обрабатывать несколько одновременных запросов, а также предоставляет расширенные возможности: кэширование запросов/ответов, объединение подключений в пул (connection pooling), аутентификация и др.
О том, почему иногда стоит использовать OkHttp, а не вездесущий Retrofit, можно посмотреть в видео от Android Broadcast. Краткое пояснение дано в следующем пункте статьи.
В статье подробно рассмотрены основные классы и методы библиотеки и представлены основы работы с ней в Android‑разработке.
Преимущества OkHttp
OkHttp — это библиотека более низкого уровня, чем Retrofit. Это означает, что HTTP‑запросы, автоматизированные в Retrofit с помощью аннотаций, придётся писать вручную. Однако в этом и главный плюс библиотеки: она предоставляет более обширный функционал и настройки соединения, что может повысить производительность и сократить использование памяти. К слову, Retrofit под капотом использует OkHttp.
Библиотека разработана как легкая и эффективная, с акцентом на снижение задержек и повышение работоспособности. Это достигается за счет применения различных методов оптимизации, таких как повторное использование соединений, сжатие и конвейеризация.
- Гибкость: Библиотека предоставляет больше контроля над процессом сетевого взаимодействием за счёт дополнительных функций, например, пользовательской обработки запросов и ответов.
- Лёгкость: OkHttp — более компактная библиотека, чем Retrofit, что позволяет минимизировать размер используемой приложением памяти.
- Кэширование: Библиотека имеет встроенную поддержку HTTP‑кэширования, что может повысить производительность и снизить нагрузку на сеть.
- Аутентификация: OkHttp предоставляет гибкий и расширяемый API аутентификации, что упрощает реализацию различных её моделей.
- Перехватчики (Interceptors): Это механизм, позволяющий легко настраивать запросы и ответы, а также хороший выбор для приложений, требующих расширенной обработки запросов.
- WebSockets: OkHttp обеспечивает встроенную поддержку WebSockets, что позволяет легко реализовать коммуникацию с сервером в режиме реального времени.
Основные классы и методы
1) Настройка клиента и запроса
Класс OkHttpClient — клиент для HTTP-вызовов, который можно использовать для отправки запросов и чтения ответов.
OkHttpClient.Builder — класс предоставляющий методы для настройки клиента, например кэш, аутентификация, перехватчики, тайм-ауты и др. По завершению настройки используется метод build(), который возвращает экземпляр класса OkHttpClient.
OkHttp работает лучше при создании одного экземпляра OkHttpClient и повторном его использовании для всех HTTP-вызовов. Так происходит потому, что каждый клиент содержит свой собственный пул соединений и пул потоков. Повторное использование соединений и потоков уменьшает задержку и экономит память. И наоборот, создание клиента для каждого запроса приводит к трате ресурсов на незадействованные пулы.
Класс Request представляет собой HTTP-запрос. Request.Builder позволяет установить параметры запроса, например url и заголовки.
В целом, HTTP-заголовки представляют собой что-то похожее на Map: каждое поле имеет одно значение или не имеет его вовсе. Однако некоторые заголовки могут иметь несколько значений. В связи с этим для добавления заголовка к запросу применяются два метода:
- header(name, value) — устанавливает только одно значение заголовка name. При этом все существующие значения заголовка будут удалены, и после этого будет установлено новое значение.
- addHeader(name, value) — добавляет заголовок без удаления уже имеющихся значений.
При чтении заголовка из ответа используйте header(name), чтобы вернуть последнее вхождение заголовка (зачастую это единственное вхождение). Если значение отсутствует, header(name) вернет null. Чтобы прочитать все значения заголовка в виде списка, используйте headers(name).
Для установки целевого URL-адреса запроса используется метод url(). По завершению настройки запроса используется метод build(), который возвращает объект Request.
2) Отправка запроса
newCall — метод класса OkHttpClient, который подготавливает запрос к выполнению в будущем. Принимает объект Request и возвращает объект Call.
Класс Call (вызов) — это запрос, который был подготовлен к выполнению. Вызов может быть отменен. Поскольку экземпляр класса представляет одну пару запрос/ответ, он не может быть выполнен дважды. Для выполнения запроса существуют два метода:
- execute() — при синхронном вызове. Метод незамедлительно выполняет запрос и блокирует поток до тех пор, пока ответ не будет доступен для обработки или пока не возникнет ошибка.
- enqueue() — при асинхронном вызове. Метод назначает запрос на выполнение в определенный момент в будущем. Диспетчер определяет, когда будет выполнен запрос: обычно сразу же, если в данный момент не выполняются несколько других запросов. Позже клиент получает объект responseCallback либо с HTTP-ответом, либо с исключением в случае возникновения ошибки.
3) Чтение ответа
Класс Response представляет HTTP-ответ. Тело ответа — свойство экземпляра класса, которое может быть использовано только один раз и затем закрыто. Все остальные свойства неизменяемы.
Прежде чем как-либо использовать тело ответа, необходимо проверить, был ли запрос к серверу успешен. Для этого существует метод isSuccessful() вышеупомянутого класса. Метод проверяет код состояния (status code) HTTP-ответа и возвращает значение true, если код находится в диапазоне 200-300. Если код находится за пределами этого диапазона, он возвращает значение false, указывающее, что запрос не был успешным.
Неуспешный запрос означает, что возникли проблемы на стороне клиента или сервера. Например, запрос был неправильно составлен, на сервере произошла ошибка или сервер некорректно обработал запрос. Если не проверять код состояния, то в конечном счёте можно работать с ответом, который не содержит ожидаемых данных.
Для получения тела ответа используется метод body() класса Response, который возвращает экземпляр класса ResponseBody.
ResponseBody — одноразовый поток от сервера к клиенту, содержащий тело ответа в виде необработанных байтов. Каждое тело ответа поддерживается активным подключением к веб-серверу.
Класс ResponseBody поддерживает потоковую передачу очень больших ответов. Например, его можно использовать для чтения ответа, размер которого превышает всю память, выделенную текущему процессу. Можно даже передавать в потоковом режиме ответ, объем которого превышает общий объем памяти на текущем устройстве, что является обычным требованием для стриминговых видео-приложений.
Класс не загружает весь ответ в память, поэтому тело ответа может быть считано только один раз. Для этого существует несколько методов:
- bytes() и string() — считывают весь текст ответа в память, а затем возвращают его в виде массива байтов или строки соответственно. Методы следует использовать только для небольших ответов. При считывании больших ответов будет вызвана ошибка OutOfMemoryError.
- source, byteStream, charStream — предназначены для потокового чтения ответа. Метод source возвращает объект BufferedSource, позволяющий читать тело ответа в виде потока байтов. byteStream работает аналогично, но возвращает объект InputStream. charStream — возвращает объект Reader, который позволяет читать тело ответа в виде потока символов.
Если использовать body() без упомянутых методов, то будет получен сам объект ResponseBody, с которым ничего особо не поделаешь.
Простой GET-запрос (синхронный/асинхронный)
Перед использованием библиотеки нужно добавить соответствующую зависимость в Gradle:
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
Номер последней версии можно посмотреть на Maven Central.
Синхронный запрос (Java):
OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url("https://publicobject.com/helloworld.txt") .build(); try (Response response = client.newCall(request).execute()) < if (!response.isSuccessful()) < throw new IOException("Запрос к серверу не был успешен: " + response.code() + " " + response.message()); >// пример получения конкретного заголовка ответа System.out.println("Server: " + response.header("Server")); // вывод тела ответа System.out.println(response.body().string()); > catch (IOException e)
Синхронный запрос (Kotlin):
val client = OkHttpClient() val request = Request.Builder() .url("https://publicobject.com/helloworld.txt") .build() try < client.newCall(request).execute().use < response ->if (!response.isSuccessful) < throw IOException("Запрос к серверу не был успешен:" + " $$") > // пример получения конкретного заголовка ответа println("Server: $") // вывод тела ответа println(response.body. string()) > > catch (e: IOException)
В то время как в Java используются методы объектов, в Kotlin иногда используются их свойства. Например, свойство body объекта Response.
Каждое тело ответа поддерживается ограниченным ресурсом. Поэтому после использования оно должно быть закрыто. Закрытие ресурса освобождает все системные средства, которые были выделены ресурсу, и делает его доступным для сбора мусора (garbage collection). Если не закрыть тело ответа, произойдет утечка ресурсов, что в конечном итоге может привести к замедлению или крашу приложения.
Для закрытия ресурса можно использовать метод close(), но предпочтительнее использовать блок try-with-resources (Java) и метод use (Kotlin). Обе конструкции выполняют блок кода относительно заданного ресурса, а затем корректно закрывают его, независимо от того, вызвано исключение или нет.
Асинхронный запрос (Java):
OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url("http://publicobject.com/helloworld.txt") .build(); client.newCall(request).enqueue(new Callback() < @Override public void onFailure(Call call, IOException e) < e.printStackTrace(); >@Override public void onResponse(Call call, Response response) throws IOException < try (ResponseBody responseBody = response.body()) < if (!response.isSuccessful()) < throw new IOException("Запрос к серверу не был успешен: " + response.code() + " " + response.message()); >// пример получения всех заголовков ответа Headers responseHeaders = response.headers(); for (int i = 0, size = responseHeaders.size(); i < size; i++) < // вывод заголовков System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i)); >// вывод тела ответа System.out.println(responseBody.string()); > > >);
Асинхронный запрос (Kotlin):
val client = OkHttpClient() val request = Request.Builder() .url("http://publicobject.com/helloworld.txt") .build() client.newCall(request).enqueue(object : Callback < override fun onFailure(call: Call, e: IOException) < e.printStackTrace() >override fun onResponse(call: Call, response: Response) < response.use < if (!response.isSuccessful) < throw IOException("Запрос к серверу не был успешен:" + " $$") > // пример получения всех заголовков ответа for ((name, value) in response.headers) < println("$name: $value") >// вывод тела ответа println(response.body. string()) > > >)
Асинхронный запрос выполняется в потоке Worker. Когда ответ доступен для чтения выполняется обратный вызов (сallback). Этот вызов выполнится после того, как будут готовы заголовки ответа. Чтение тела ответа все еще может блокировать поток. OkHttp в настоящее время не предлагает асинхронных API для получения тела ответа по частям.
Callback имеет два абстрактных метода:
- onResponse — вызывается, когда HTTP-ответ был успешно получен от удаленного сервера.
- onFailure — вызывается, когда запрос не может быть выполнен из-за проблем с подключением, тайм-аута или при его отмене. Поскольку в сети может произойти сбой во время соединения с сервером, возможен случай, когда удаленный сервер успевает принять запрос до сбоя.
Сериализация/десериализация
В данном пункте кратко рассмотрена сериализация и десериализация объектов (их преобразование в определённую последовательность байтов, которую можно передать по сети, и наоборот).
Для того, чтобы преобразовать объект в строку JSON или наоборот можно воспользоваться библиотеками Gson и/или Moshi.
Вкратце, если вам нужна проста использования и широкий набор функций, то выбираете Gson. Если нужна производительность и эффективное использование памяти, то лучшим выбором будет Moshi.
Рассмотрим пример сериализации с помощью Moshi (Java):
// Создание объекта Moshi Moshi moshi = new Moshi.Builder().build(); // Создание адаптера JsonAdapter jsonAdapterRequest = moshi.adapter(SomeClass.class); // Сериализация, SomeClassInstance - экземляр класса SomeClass String jsonRequest = jsonAdapterRequest.toJson(SomeClassInstance);
То же самое в Kotlin:
// Создание объекта Moshi, // для Kotlin необходимо добавлять KotlinJsonAdapterFactory val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()).build() // Создание адаптера val jsonAdapterRequest = moshi.adapter(SomeClass::class.java) // Сериализация, SomeClassInstance - экземляр класса SomeClass val jsonRequest = jsonAdapterRequest.toJson(SomeClassInstance)
Для сериализации необходимо создать объект Moshi, адаптер и передать ему тип сериализуемого объекта. В данном случае это тип Class.
Если требуется сериализовать более сложный объект, например коллекцию, то тип можно передать двумя способами.
1) С помощью метода Types.newParameterizedType(), который создает новый параметризованный тип.
// Java JsonAdapter> jsonAdapterRequest = moshi.adapter( Types.newParameterizedType(List.class, SomeClass.class) );
// Kotlin val jsonAdapterRequest = moshi.adapter>( Types.newParameterizedType(List::class.java, SomeClass::class.java)
2) С помощью класса TypeToken библиотеки Gson. Класс используется для передачи информации о типах во время выполнения программы. Конструктор класса возвращает представленный класс из заданного типа.
// Java JsonAdapter> jsonAdapterRequest = moshi.adapter(new TypeToken>()<>.getType());
// Kotlin val jsonAdapterRequest = moshi.adapter>( object : TypeToken?>()<>.type )
Разница способов состоит в том, что TypeToken более типобезопасен (typesafe), а Types.newParameterizedType более эффективен.
Десериализация осуществляется аналогичным образом.
// Java // Создание адаптера для десериализации // Используется тот же объект Moshi, что и при сериализации JsonAdapter jsonAdapterResponse = moshi.adapter(SomeClass.class); // Десериализация String jsonResponse = jsonAdapterResponse.fromJson(receivedData);
// Kotlin // Создание адаптера для десериализации // Используется тот же объект Moshi, что и при сериализации val jsonAdapterResponse = moshi.adapter(SomeClass::class.java) // Десериализация val jsonResponse = jsonAdapterResponse.fromJson(receivedData)
При сериализации/десериализации Moshi может вызывать разного рода исключения, к примеру если десериализуемая строка не является строкой JSON или если строка не соответствует объекту, в который её пытаются преобразовать.
Если серверная и клиентская часть настроены правильно, то такого не должно происходить. Но всё же рекомендуется оборачивать операции Moshi в блок try-catch.
Простой POST-запрос
Чтобы сделать POST-запрос, используется метод post() класса Request.Builder. Метод принимает RequestBody, который он добавляет к запросу.
POST-запрос в Java:
MediaType JSON = MediaType.get("application/json; charset=utf-8"); String jsonRequest = "Some request"; OkHttpClient client = new OkHttpClient(); RequestBody body = RequestBody.create(jsonRequest, JSON); Request.Builder requestBuilder = new Request.Builder().url(serverUrl).post(body); Request request = requestBuilder.build(); try (Response response = client.newCall(request).execute()) < if (!response.isSuccessful()) < throw new IOException("Запрос к серверу не был успешен: " + response.code() + " " + response.message()); >System.out.println(response.body().string()); > catch (IOException e)
POST-запрос в Kotlin:
val jsonRequest = "some request" val JSON = "application/json; charset=utf-8".toMediaType() val client = OkHttpClient() val body: RequestBody = jsonRequest.toRequestBody(JSON) val request = Request.Builder().url(serverUrl).post(body).build() try < client.newCall(request).execute().use < response ->if (!response.isSuccessful) < throw IOException("Запрос к серверу не был успешен:" + " $$") > println(response.body. string()) > > catch (e: IOException)
Объект MediaType необходим для описания типа содержимого тела запроса или ответа. Обычно он используется для установки заголовка «Content-Type» в HTTP-запросе.
Чтобы получить объект MediaType можно использовать один из статических методов одноименного класса:
- MediaType.parse(String) — создает новый экземпляр MediaType с указанным типом содержимого и кодировкой. Функция возвращает медиатип для строки, или null, если строка не является правильно сформированным медиатипом.
- MediaType.get(String) — работает аналогично MediaType.parse, но если строка сформирована неправильно, то вызывает исключение IllegalArgumentException.
В Kotlin используется метод toMediaType() объекта String. Метод является аналогом MediaType.get(String).
RequestBody — класс, представляющий собой тело запроса. Экземпляр класса создаётся с помощью метода create.
RequestBody.create(MediaType, String) создает тело запроса с указанным содержимым и его типом. Метод имеет несколько реализаций. Содержимое можно передать в виде массива байтов, файла, строки или объекта okio.ByteString. Тип содержимого всегда указывается с помощью объекта MediaType. Этот объект также устанавливает заголовку «Content-type» соответствующее значение, поэтому вручную устанавливать этот заголовок не нужно.
Аналогом RequestBody.create(MediaType, String) в Kotlin является метод toRequestBody(MediaType?) объекта String.
Особенности работы с HTTPS
OkHttp пытается балансировать между двумя задачами:
- Подключение к максимально возможному количеству хостов. Сюда входят как современные хосты, на которых используются последние версии boringssl, так и немного устаревшие хосты, на которых используются старые версии OpenSSL.
- Безопасность соединения. Сюда входит проверка удаленного веб-сервера с помощью сертификатов и конфиденциальность данных, передаваемых с помощью надежных шифров.
При согласовании соединения с HTTPS-сервером OkHttp должен знать, какие предлагать версии TLS и наборы шифров. Для клиента, который хочет максимизировать возможность соединения с различными серверами, это будут устаревшие версии TLS и слабые по конструкции наборы шифров. Для клиента, который хочет максимизировать безопасность, это будут только последняя версия TLS и самые сильные наборы шифров.
Конкретные решения по безопасности и соединению реализуются с помощью ConnectionSpec. OkHttp включает четыре встроенных типа соединений:
- RESTRICTED_TLS — безопасная конфигурация, предназначенная для удовлетворения более строгих требований по соответствию.
- MODERN_TLS — безопасная конфигурация, позволяющая подключаться к современным HTTPS-серверам.
- COMPATIBLE_TLS — безопасная конфигурация, которая подключается к безопасным, но менее современным серверам HTTPS.
- CLEARTEXT — небезопасная конфигурация, которая используется для URL-адресов http://.
По умолчанию OkHttp будет пытаться установить соединение MODERN_TLS. Если соединение MODERN_TLS не удастся, okhttp3 переключится на другой тип соединения. Точный механизм отката зависит от конкретной реализации okhttp3 и конфигурации, установленной разработчиками.
Настроить конфигурацию можно следующим образом:
OkHttpClient client = new OkHttpClient.Builder() .connectionSpecs(Arrays.asList( ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS)) .build()
В официальной документации можно найти дополнительные способы работы с HTTPS, такие как создание собственной спецификации подключения, закрепление сертификата и настройка доверенных сертификатов.
Аутентификация на сервере
Аутентификацию на сервере можно реализовать двумя способами.
1) Вручную добавить заголовок аутентификации. Полезно в случае, если нужна аутентификация только для одного запроса. Для того, чтобы добавлять заголовок ко всем запросам клиента, можно создать перехватчик. Способ полезен, если у вас статический ключ API или токен, который нужно отправлять с каждым запросом.
// Kotlin val client = OkHttpClient().newBuilder().addInterceptor < chain ->val originalRequest = chain.request() val builder = originalRequest.newBuilder() .header("Authorization", Credentials.basic("username", "password")) val newRequest = builder.build() chain.proceed(newRequest) >.build()
2) Использовать интерфейс Authenticator — полезно, когда необходимо динамически аутентифицироваться или нужна дополнительная настройка процесса аутентификации.
Интерфейс позволяет выполнить либо предварительную аутентификацию перед подключением к серверу, либо реактивную аутентификацию после получения ответа от веб-сервера или прокси-сервера.
Рассмотрим пример реактивной аутентификации. В таком случае, если код состояния ответа равен 401 (Unauthorized), OkHttp посылает повторный запрос, включающий заголовок «Authorization».
При этом важно сделать проверку, была ли в первоначальном запросе попытка аутентификации. Если да, то, скорее всего, дальнейшие попытки будут бесполезны, и аутентификатор должен отказаться от них.
// Java OkHttpClient.Builder client = new OkHttpClient.Builder(); client.authenticator((route, response) -> < if (response.request().header("Authorization") != null) < return null; // Остановить попытки аутентификации, // т.к. у нас уже не получилось это сделать >String credential = Credentials.basic("name", "password"); return response.request().newBuilder() .header("Authorization", credential).build(); >);
Здесь метод authenticator с помощью лямбда-функции устанавливает экземпляр интерфейса Authenticator, который предоставляет механизм для проверки ответа от сервера и возвращает запрос, включающий в себя учетные данные клиента. Метод Credentials.basic используется для кодирования имени пользователя и пароля при базовой аутентификации.
Использование вместе с ViewModel
Простой асинхронный запрос в ViewModel можно сделать следующим образом.
// MainViewModel.kt private val _response = MutableLiveData() val response: LiveData = _response private val client = OkHttpClient() fun getResponseFromServer() < val jsonRequest = "your request body" val body = jsonRequest.toRequestBody(JSON) val request = Request.Builder().url(BASE_URL).post(body).build() client.newCall(request).enqueue(object : Callback < override fun onFailure(call: Call, e: IOException) < Log.d(TAG, "Ошибка подключения: $e") >override fun onResponse(call: Call, response: Response) < response.use < if (!response.isSuccessful) < throw Exception("Запрос к серверу не был успешен:" + " $$") > _response.postValue(response.body. string()) > > >) >
Метод postValue передаёт задачу по установке значения главному потоку. Если попытаться присвоить значение напрямую, то будет вызвано исключение java.lang.IllegalStateException: Cannot invoke setValue on a background thread.
Необходимо делать именно асинхронный запрос, чтобы не блокировать поток интерфейса и чтобы приложение оставалось отзывчивым. Либо можно самостоятельно настроить синхронный вызов в другом потоке.
Начиная с SDK 10 при попытке синхронного вызова в главном потоке будет вызвано исключение android.os.NetworkOnMainThreadException.
Чтобы сделать запрос в отдельном api-файле и передать ответ переменной из ViewModel можно воспользоваться механизмом callback.
// SomeApiService.kt interface RequestCallback < fun onSuccess(response: String) fun onFailure(error: String) >class SomeApiService () < private val client = OkHttpClient() fun makeRequest(callback: RequestCallback) < val jsonRequest = "your request body" val body = jsonRequest.toRequestBody(JSON) val request = Request.Builder().url(BASE_URL).post(body).build() client.newCall(request).enqueue(object : Callback < override fun onFailure(call: Call, e: IOException) < callback.onFailure(e.toString()); >override fun onResponse(call: Call, response: Response) < response.use < if (!response.isSuccessful) < throw Exception("Запрос к серверу не был успешен:" + " $$") > callback.onSuccess(response.body. string()) > > >) > > object SomeApi < val someService : SomeApiService by lazy < SomeApiService() >>
// MainViewModel.kt private val _response = MutableLiveData() val response: LiveData = _response private val client = OkHttpClient() fun getResponseFromApi() < SomeApi.someService.makeRequest(object : RequestCallback < override fun onSuccess(response: String) < _response.postValue(response) >override fun onFailure(error: String) < Log.d(TAG, "Ошибка подключения: $error") >>) >
В файле SomeApiService.kt находится интерфейс RequestCallback, класс SomeApiService с методом makeRequest(), который делает запрос к серверу, и объект SomeApi, через который будет осуществляться доступ к экземпляру класса.
В MainViewModel.kt функция getResponseFromApi() реализует интерфейс RequestCallback и передает его в качестве параметра методу makeRequest().
Тело функции getResponseFromApi() можно обернуть во viewModelScope.launch <. >, чтобы запрос был отменён при очистке (разрушении) MainViewModel.
Заключение
OkHttp — гибкая библиотека, выступающая в роли HTTP-клиента.
В отличии Retrofit настраивать клиент, писать запросы и обрабатывать ответы необходимо вручную. Это одновременно и преимущество и недостаток OkHttp. Недостаток заключается в необходимости писать много шаблонного кода. Преимущество — возможность кастомизировать соединение.
Из-за слабой кастомизируемости в некоторых случаях Retrofit может не подойти, и без OkHttp не обойтись. Также благодаря кастомизации OkHttp можно повысить производительность и уменьшить использование памяти.
How to make a string formatting server-driven in kotlin- Android?
Let’s suppose we want to change the color of a string on the basis of backend response in kotlin, and want to make some words of a string URL that will redirect to a webpage on click on it.
Let’s suppose we want to achieve something like this ^, where you can see some text has links and some in bold(eg:- Detail), now also this whole string is coming from backend,
lets suppose our response is like this:-
"string": "Read more about %s and %s in $",
"text": [
"title": "TnC",
"href": "https://www.google.com"
>,
"title": "Product Information",
"href": "https://www.linkedin.com"
>,
"title": "Detail",
"href": null
>
]
>
if you see our backend response we have given some special characters for strings that we want to change or do formatting on those strings on local and we also created one array of text which will contain the title and link’s href, so that we can make some words clickable in strings and open a webpage after click on it, if href is null we will assume that we have to that word as bold in string. you guys can make a response according to your requirement I just gave an example how we can achieve this using special characters in our string.
Its time to write the frontend kotlin code ✌️
val footerString = SpannableStringBuilder("Read more about %s and %s in $")
addFormattingToFooterString(footerString, footer)
with(binding.contentView.tvTermsAndConditions) text = footerString
linksClickable = true
movementMethod = LinkMovementMethod.getInstance()
>
As you can see in the above code we have created one spannable string, spannable strings are useful when we want to add some styles or some spans to some specific words in our string.
After that we called add formatting to string method to add styling and links to the string.
below that we made our whole textview clickable and added link movement so whenever any we add any href(url) to any word in our string, after click on it, that will open the respective web page.
private fun addFormattingToFooterString(
footerString: SpannableStringBuilder,
footer: Footer
) var linkListIndex = 0
footerString.forEachIndexed index, char ->
if(char == '$') footerString.setSpan(
StyleSpan(Typeface.BOLD),
index, index+1, Spannable.SPAN_INCLUSIVE_INCLUSIVE
)
footerString.replace(index, index+1, "Detail")
linkListIndex++ >
if (char == '%') footerString.setSpan(
StyleSpan(Typeface.BOLD),
index,
index + 2,
Spannable.SPAN_INCLUSIVE_INCLUSIVE
)
val footerTermsAndConditionString = footer.link[linkListIndex].text
val replaceableString =
" + footer.link[linkListIndex].url + "'>$footerTermsAndConditionString "
footerString.replace(
index,
index + 2,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) Html.fromHtml(replaceableString, Html.FROM_HTML_MODE_LEGACY)
> else Html.fromHtml(replaceableString)
>
)
linkListIndex++
>
>
>
In the above method, we did multiple things let’s understand all these things one by one.
1. first we created one listIndex variable for getting the index of title in the above-created text array(in backend response).
2. lets traverse our footerString and let’s check for a bold string using $ character, if it’s present in the response string, we will replace it with a backend string and will make it bold. for this first, we will set the span for this and will make the string bold between current index and current index+1(because we only want to replace word whose length is 1 right now(current index+1-currentIndex, `$`).
3. Now after this we replaced the character with our word which we want to make bold using backend response. now as we have used SPAN_INCLUSIVE_INCLUSIVE here, so this span will expand to include any newly inserted characters in the string and will make them bold also, its definition according to the documentation is:-
/**
* Spans of type SPAN_INCLUSIVE_INCLUSIVE expand
* to include text inserted at either their starting or ending point.
*/
4. Now next is we add urls to words with %s and replaced %s with text coming in backend response and add urls to them. as we have already maintained a list index variable to find the index of the replaced string in text array, we will use it as a replaced string.
5. Now we are using href’s anchor tag to add the URL to the specific word.
6. At the end we will replace our %s with Html string and it’s done.
So, as you guys can see how we designed our backend contract to make things like styling, URLs to words in string backend driven. this has so many benefits that in the future we can change as many words (change their styles, add urls to some words) on the front end without any app release.
We can also use ClickableSpan for our use case, but it creates some extra classes and also some extra code, so I went with this.
Thank you for reading this article, if you have any doubt or feedback you can reach out to me on Linkedin.
[Retrofit] [Kotlin] How to view raw response string in onFailure and How to post List
Im trying to post a List and I cant seem to figure it out. Ive been able to Post singluar values (int, string) but cant seem to do a list.
How can I view the response string before Retrofit tries to parse it into JSON. I need to do this because the script is crashing when I post from android but not from anywhere else.
Also how can I post List using retrofit?
//onFailure gets called cause the script is crashing @FormUrlEncoded @POST(«enrollClass.php») abstract fun saveCourseIds(@Field(«ACCID») accid: Int, @Field(«COURSE[]») course: List): Call //onfailure method override fun onFailure(call: Call, t: Throwable) < // call.request().body().toString() doesnt work, it prints out byte data Log.d("CLASSES FRAGMENT", t.localizedMessage + " " + call.request().body().toString()) Toast.LENGTH_SHORT).show() >
