[Разработка мобильных приложений, Разработка под Android, Kotlin, Голосовые интерфейсы] Голос в мобильном приложении: учимся вызывать экраны и заполнять формы без рук
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Как быстро и бесшовно встроить голосовой интерфейс в ваше мобильное приложение? И как научить ассистента всему, что оно умеет? В прошлый раз мы взяли опенсорсное лайфстайл-приложение Habitica и показали, как добавить в него помощника и запилить базовый голосовой сценарий «из коробки» (уточнение прогноза погоды и времени). А теперь перейдем к более продвинутому этапу – научимся вызывать голосом определенные экраны, делать сложные запросы с NLU и form-filling с помощью голоса внутри приложения.
(Читать первую часть туториала)Итак, Habitica – это приложение для выработки хороших привычек с элементами геймификации: поддержание ваших жизненных целей в виде привычек, ежедневных дел и задач поощряется наградами. И сейчас мы научим голосового ассистента, которого сами же в приложение и поселили, как создавать и заполнять таски, вредные привычки и награды голосом, а не вручную.Логика голосового интерфейсаНачнем с самого простого – логики на стороне приложения. Мы хотим по голосовой команде открывать, например, настройки или окно изменения характеристик. Открываем AndroidManifest и находим соответствующие активити. Находим PrefsActivity, который отвечает за настройки, FixCharacterValuesActivity, который отвечает за изменение характеристик персонажа, и до кучи находим активити, по которой открывается профиль и информация о приложении, FullProfileActivity и AboutActivity.Согласно документации, нам нужно вносить клиентскую логику в класс, наследуемый от CustomSkill. Во-первых, укажем, что нам нужно реагировать только на ответ от бота, содержащий в response.action “changeView”. В response.intent мы будем передавать непосредственно команду, куда именно переходить – и в зависимости от этого вызывать активити. Ну и не забудем перед этим найти контекст приложения:
class ChangeViewSkill(private val context: Context): CustomSkill<AimyboxRequest, AimyboxResponse> {
override fun canHandle(response: AimyboxResponse) = response.action == "changeView"
override suspend fun onResponse(
response: AimyboxResponse,
aimybox: Aimybox,
defaultHandler: suspend (Response) -> Unit
) {
val intent = when (response.intent) {
"settings" -> Intent(context, PrefsActivity::class.java)
"characteristics" -> Intent(context, FixCharacterValuesActivity::class.java)//
"profile" -> Intent(context, FullProfileActivity::class.java)//
"about" -> Intent(context, AboutActivity::class.java)
else -> Intent(context, MainActivity::class.java)
}
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
aimybox.standby()
context.startActivity(intent)
}
}
Этот скилл добавляется к ассистенту следующим образом:
val dialogApi = AimyboxDialogApi(
"YOUR KEY HERE", unitId,
customSkills = linkedSetOf(ChangeView()))
Навык и интентыНавык мы будем писать на JAICF (это опенсорсный и совершенно бесплатный фреймворк для разработки голосовых приложений от Just AI на Kotlin).Форкаем себе https://github.com/just-ai/jaicf-jaicp-caila-template.К сожалению, на момент написания статьи на платформе JAICP (Just AI Conversational Platform) еще не было интеграции c Aimybox (SDK для построения диалоговых интерфейсов), иначе подключение было бы намного более простым – просто через добавление одной строчки в один из двух файлов подключений в папке connections. А пока делаем новый файл подключения, который мы будем запускать для тестов. Создаем файл AimyboxConnection.
package com.justai.jaicf.template.connections
import com.justai.jaicf.channel.http.httpBotRouting
import com.justai.jaicf.channel.aimybox.AimyboxChannel
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import com.justai.jaicf.template.templateBot
fun main() {
embeddedServer(Netty, System.getenv("PORT")?.toInt() ?: 8080) {
routing {
httpBotRouting("/" to AimyboxChannel(templateBot))
}
}.start(wait = true)
}
Для того, чтобы пользоваться NLU-функционалом, подключаем NLU-сервис Caila – для этого регистрируемся на app.jaicp.com, в настройках находим ключ API и прописываем его в conf/jaicp.properties. Теперь мы можем прямо в сценарии ссылаться на интенты, которые пропишем на app.jaicp.com.
Можно воспользоваться любым другим NLU-функционалом или обойтись регулярными выражениями – но для того, чтобы сделать все красиво и просто для пользователя, лучше пользоваться NLU.Для начала заведем интенты. Нам нужно распознавать, что пользователь хочет перейти в определенный раздел приложения. Для этого в сущностях мы заводим сущность под каждый из разделов, добавляя синонимы, и в DATA прописываем то, как мы будем распознавать это уже на уровне приложения (settings, characteristics, и т.д. из кода выше).У меня получилось вот так:
Дальше прописываем то, как именно мы ожидаем встретить эту сущность во фразах пользователя. Для этого создаем интент и прописываем там вариации фраз. Кроме того, так как для перехода нам обязательно нужно знать, куда переходить, прописываем, что содержание сущности views во фразе обязательное. У меня получилось так.
По названию мы потом будем отсылать к этому интенту в коде JAICF.Чтобы удостовериться, что интенты распознаются как надо, можно сразу ввести несколько тест-фраз по кнопке «Тестирование» . Вроде все ок.
Сценарий: вызываем скиллЯ на всякий случай потер все стандартные стейты, оставив только catchAll – то, что бот говорит, если он нас не понимает. Создаем стейт changeView, в activators прописываем созданный нами в JAICP интент, а в actions прописываем логику – нам нужно добавить в ответ бота, в стандартные реакции канала Aimybox всю информацию для того, чтобы сделать переход.Просто достаем слот views из того, что распознала Caila, прописываем в action то, что мы прописали ранее, чтобы Aimybox знал, какой скилл запустить, и отправляем распознанный слот в интенте. Для красоты добавляем туда «Перехожу». Все-таки ж чатбот.
state("changeView") {
activators {
intent("changeView")
}
action {
reactions.say("Перехожу..." )
var slot = ""
activator.caila?.run {slot = slots["views"].toString()}
reactions.aimybox?.response?.action = "changeView"
reactions.aimybox?.response?.intent = slot
}
}
Скиллы лучше выносить в отдельный пакет skills с фаликом класса под каждый скилл.
Дальше вариантов несколько. Можно поднять бота локально через ngrok, можно воспользоваться heroku. Получившуюся ссылку прокидываем в app.aimybox.com, через создание там кастомного навыка, в поле Aimylogic webhook URL. В примеры пишем пару примеров вызова: открой настройки, открой инфо.
После подключения канала можно проверить выдачу прямо в консоли, чтобы отловить баги, по кнопке Try in Action.Можно подключить скилл напрямую, без консоли и дополнительных навыков – как, описано тут.Вроде все передается правильно. Попробуем в приложении. Весь код уже готов, осталось только запустить и попробовать.Извините, данный ресурс не поддреживается. :( Работает! Теперь самое сложное. Заполняем задачи голосомХочется одной командой заполнить задачку, проверить, что все правильно, исправить какие-то небольшие ошибки (все-таки распознавание не всегда работает идеально), и только после этого создать ее окончательно.Для этого сделаем второй скилл. Будем отличать его от первого через response.action == "createTask", а то, какой конкретно тип задачки создается через response.intent. Изучив сорцы приложения, понимаешь, что и награды, и дэйлики, и привычки, и задачки создаются через TaskFormActivity, просто с разными типами. Для начала пропишем эту логику.
class CreateTaskSkill(private val context: Context): CustomSkill<AimyboxRequest, AimyboxResponse> {
override fun canHandle(response: AimyboxResponse) = response.action == "createTask"
override suspend fun onResponse(
response: AimyboxResponse,
aimybox: Aimybox,
defaultHandler: suspend (Response) -> Unit
) {
val intent = Intent(context, TaskFormActivity::class.java)
val additionalData = HashMap<String, Any>()
val type = response.intent
additionalData["viewed task type"] = when (type) {
"habit" -> Task.TYPE_HABIT
"daily" -> Task.TYPE_DAILY
"todo" -> Task.TYPE_TODO
"reward" -> Task.TYPE_REWARD
else -> ""
}
В каждой из тасок (включая награды) есть название и описание, также есть сложность у задач и вредность у привычек. Давайте научимся прокидывать их.Передавать их мы будем через response.data, если они будут нулевыми, проставим стандартное описание.Забандлим полученные данные и запустим таску с этим бандлом. Не забудем добавить обработку забандленного кода в onCreate TaskFormActivity.
// Inserted code for voice activation
textEditText.setText(bundle.getString("activity_name")) // presetting task name
notesEditText.setText(bundle.getString("activity_description")) //presetting task description
if (bundle.getBoolean("sentiment")) { // presetting task sentiment
habitScoringButtons.isPositive = true
habitScoringButtons.isNegative = false
} else {
habitScoringButtons.isNegative = true
habitScoringButtons.isPositive = false
}
when (bundle.getString("activity_difficulty").toString()) { // presetting task difficulty
"trivial" -> taskDifficultyButtons.selectedDifficulty = 0.1f
"easy" -> taskDifficultyButtons.selectedDifficulty = 1f
"medium" -> taskDifficultyButtons.selectedDifficulty = 1.5f
"hard" -> taskDifficultyButtons.selectedDifficulty = 2f
else -> taskDifficultyButtons.selectedDifficulty = 1f
}
Теперь настроим распознавание и передачу в коде JAICF и в Caila.Готовим Caila: заводим сущность под распознавание типов тасок, сложности и вредности (для примера я завел их с помощью паттернов, для этого нужно выбрать Pattern вместо синонимов в левой части формы).
Не забываем в data прописать данные, которые мы будем обрабатывать на клиентской стороне – habit, pattern и так далее.Так как название и описание может быть любым, создадим сущности Name и Description, в которой пропишем регулярное выражение, матчащее любое слово. Пока что у нас в названии и описании будет по одному слову.
Делаем интент:
Указываем, что нам обязательно нужен task_type и сложность. Можем добавить в обязательные и название, и описание – тогда, если пользователь не скажет одно или другое, бот уточнит у него с помощью вопроса слот, который еще не указан.
Прописываем разные вариации того, как можно задать название и описание вместе с типом (порядок, отсутствие одного или другого). Тут нет предела совершенству, но для минимума достаточно шаблонов выше.Также для примера здесь я использую язык шаблонов, который можно изменить по нажатию на кнопку слева от ввода. @ – шаблоны и регулярки, “ – примеры и семантическая близость.
Теперь сценарий в JAICF.
state("createTask") {
activators {
intent("createTask")
}
action {
val taskType = activator.getCailaSlot("taskType").asJsonLiteralOr("")
reactions.say("Перехожу...")
reactions.aimybox?.response?.action = "createTask"
reactions.aimybox?.response?.intent = taskType.content
reactions.aimybox?.response?.run {
data["taskName"] = activator.getCailaSlot("taskName").asJsonLiteralOr("")
data["taskDescription"] = activator.getCailaSlot("taskDescription").asJsonLiteralOr("")
data["taskSentiment"] = activator.getCailaSlotBool("taskSentiment").asJsonLiteralOr(true)
data["taskDifficulty"] = activator.getCailaSlot("taskDifficulty").asJsonLiteralOr("easy")
}
}
}
private fun ActivatorContext.getCailaRequiredSlot(k: String): String =
getCailaSlot(k) ?: error("Missing Caila slot for key: $k")
private fun ActivatorContext.getCailaSlot(k: String): String? =
caila?.slots?.get(k)
private fun ActivatorContext.getCailaSlotBool(k: String): Boolean? =
caila?.slots?.get(k)?.toBoolean()
private fun String?.asJsonLiteralOr(other: String) = this?.let { JsonLiteral(this) } ?: JsonLiteral(other)
private fun Boolean?.asJsonLiteralOr(other: Boolean) = this?.let { JsonLiteral(this) } ?: JsonLiteral(other)
Подключаем интент через активатор, записываем из полученных слотов тип в intent, название и описание в data, и не забываем проставить action, чтобы Aimybox с клиентской стороны знал, какой скилл выбрать.Проверяем, работает! Предлагаю включить звук и прочекать:Извините, данный ресурс не поддреживается. :( Извините, данный ресурс не поддреживается. :( Извините, данный ресурс не поддреживается. :( Да, это техническое демо – конечно, с точки зрения продукта можно придумать сценарии поудобнее. Но об этом в следующих статьях!
Ссылка на репозиторий с навыком JAICF.
Ссылка на репозиторий с кодом Aimybox.
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка мобильных приложений, Разработка под Android] Еще раз про многомодульность Android-приложений
- [Разработка мобильных приложений, Разработка под Android, Смартфоны, Софт, Социальные сети и сообщества] В бете Telegram появились комментарии к постам
- [Разработка мобильных приложений, Flutter] Flutter: The Best Cross-Platform Framework For App Development
- [IPTV, Видеоконференцсвязь, Разработка под Linux, Разработка под Android, Производство и разработка электроники] Как разработать аналог Zoom для ТВ-приставок на RDK и Linux. Разбираемся с фреймворком GStreamer
- [Разработка под Android] Бесшовные A/B-обновления в Android: как они устроены
- [IT-компании, Копирайт, Разработка мобильных приложений, Разработка под Android] Со следующего года все приложения Google Play будут платить комиссию 30 %, включая Netflix и Spotify
- [Flutter, Разработка мобильных приложений] InheritedWidget во Flutter (перевод)
- [Flutter, Конференции, Разработка мобильных приложений, Разработка под Android, Разработка под iOS] Нативная разработка vs кроссплатформенная – обсуждаем 30 сентября с владельцами приложений
- [Разработка мобильных приложений, Flutter] «Flutter клёвенький — у меня только такое объяснение». Обзор лучших выпусков Flutter Dev Podcast
- [Разработка под Android] Google Play In-App Review API: пошаговое руководство по внедрению
Теги для поиска: #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_razrabotka_pod_android (Разработка под Android), #_kotlin, #_golosovye_interfejsy (Голосовые интерфейсы), #_kotlin, #_jaicf, #_voice_assistant, #_mobile_apps, #_nlu, #_voice_ui, #_aimybox, #_mobile_development, #_blog_kompanii_just_ai (
Блог компании Just AI
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_razrabotka_pod_android (
Разработка под Android
), #_kotlin, #_golosovye_interfejsy (
Голосовые интерфейсы
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:38
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Как быстро и бесшовно встроить голосовой интерфейс в ваше мобильное приложение? И как научить ассистента всему, что оно умеет? В прошлый раз мы взяли опенсорсное лайфстайл-приложение Habitica и показали, как добавить в него помощника и запилить базовый голосовой сценарий «из коробки» (уточнение прогноза погоды и времени). А теперь перейдем к более продвинутому этапу – научимся вызывать голосом определенные экраны, делать сложные запросы с NLU и form-filling с помощью голоса внутри приложения. (Читать первую часть туториала)Итак, Habitica – это приложение для выработки хороших привычек с элементами геймификации: поддержание ваших жизненных целей в виде привычек, ежедневных дел и задач поощряется наградами. И сейчас мы научим голосового ассистента, которого сами же в приложение и поселили, как создавать и заполнять таски, вредные привычки и награды голосом, а не вручную.Логика голосового интерфейсаНачнем с самого простого – логики на стороне приложения. Мы хотим по голосовой команде открывать, например, настройки или окно изменения характеристик. Открываем AndroidManifest и находим соответствующие активити. Находим PrefsActivity, который отвечает за настройки, FixCharacterValuesActivity, который отвечает за изменение характеристик персонажа, и до кучи находим активити, по которой открывается профиль и информация о приложении, FullProfileActivity и AboutActivity.Согласно документации, нам нужно вносить клиентскую логику в класс, наследуемый от CustomSkill. Во-первых, укажем, что нам нужно реагировать только на ответ от бота, содержащий в response.action “changeView”. В response.intent мы будем передавать непосредственно команду, куда именно переходить – и в зависимости от этого вызывать активити. Ну и не забудем перед этим найти контекст приложения: class ChangeViewSkill(private val context: Context): CustomSkill<AimyboxRequest, AimyboxResponse> {
override fun canHandle(response: AimyboxResponse) = response.action == "changeView" override suspend fun onResponse( response: AimyboxResponse, aimybox: Aimybox, defaultHandler: suspend (Response) -> Unit ) { val intent = when (response.intent) { "settings" -> Intent(context, PrefsActivity::class.java) "characteristics" -> Intent(context, FixCharacterValuesActivity::class.java)// "profile" -> Intent(context, FullProfileActivity::class.java)// "about" -> Intent(context, AboutActivity::class.java) else -> Intent(context, MainActivity::class.java) } intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) aimybox.standby() context.startActivity(intent) } } val dialogApi = AimyboxDialogApi(
"YOUR KEY HERE", unitId, customSkills = linkedSetOf(ChangeView())) package com.justai.jaicf.template.connections
import com.justai.jaicf.channel.http.httpBotRouting import com.justai.jaicf.channel.aimybox.AimyboxChannel import io.ktor.routing.routing import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty import com.justai.jaicf.template.templateBot fun main() { embeddedServer(Netty, System.getenv("PORT")?.toInt() ?: 8080) { routing { httpBotRouting("/" to AimyboxChannel(templateBot)) } }.start(wait = true) } Можно воспользоваться любым другим NLU-функционалом или обойтись регулярными выражениями – но для того, чтобы сделать все красиво и просто для пользователя, лучше пользоваться NLU.Для начала заведем интенты. Нам нужно распознавать, что пользователь хочет перейти в определенный раздел приложения. Для этого в сущностях мы заводим сущность под каждый из разделов, добавляя синонимы, и в DATA прописываем то, как мы будем распознавать это уже на уровне приложения (settings, characteristics, и т.д. из кода выше).У меня получилось вот так: Дальше прописываем то, как именно мы ожидаем встретить эту сущность во фразах пользователя. Для этого создаем интент и прописываем там вариации фраз. Кроме того, так как для перехода нам обязательно нужно знать, куда переходить, прописываем, что содержание сущности views во фразе обязательное. У меня получилось так. По названию мы потом будем отсылать к этому интенту в коде JAICF.Чтобы удостовериться, что интенты распознаются как надо, можно сразу ввести несколько тест-фраз по кнопке «Тестирование» . Вроде все ок. Сценарий: вызываем скиллЯ на всякий случай потер все стандартные стейты, оставив только catchAll – то, что бот говорит, если он нас не понимает. Создаем стейт changeView, в activators прописываем созданный нами в JAICP интент, а в actions прописываем логику – нам нужно добавить в ответ бота, в стандартные реакции канала Aimybox всю информацию для того, чтобы сделать переход.Просто достаем слот views из того, что распознала Caila, прописываем в action то, что мы прописали ранее, чтобы Aimybox знал, какой скилл запустить, и отправляем распознанный слот в интенте. Для красоты добавляем туда «Перехожу». Все-таки ж чатбот. state("changeView") {
activators { intent("changeView") } action { reactions.say("Перехожу..." ) var slot = "" activator.caila?.run {slot = slots["views"].toString()} reactions.aimybox?.response?.action = "changeView" reactions.aimybox?.response?.intent = slot } } Дальше вариантов несколько. Можно поднять бота локально через ngrok, можно воспользоваться heroku. Получившуюся ссылку прокидываем в app.aimybox.com, через создание там кастомного навыка, в поле Aimylogic webhook URL. В примеры пишем пару примеров вызова: открой настройки, открой инфо. После подключения канала можно проверить выдачу прямо в консоли, чтобы отловить баги, по кнопке Try in Action.Можно подключить скилл напрямую, без консоли и дополнительных навыков – как, описано тут.Вроде все передается правильно. Попробуем в приложении. Весь код уже готов, осталось только запустить и попробовать.Извините, данный ресурс не поддреживается. :( Работает! Теперь самое сложное. Заполняем задачи голосомХочется одной командой заполнить задачку, проверить, что все правильно, исправить какие-то небольшие ошибки (все-таки распознавание не всегда работает идеально), и только после этого создать ее окончательно.Для этого сделаем второй скилл. Будем отличать его от первого через response.action == "createTask", а то, какой конкретно тип задачки создается через response.intent. Изучив сорцы приложения, понимаешь, что и награды, и дэйлики, и привычки, и задачки создаются через TaskFormActivity, просто с разными типами. Для начала пропишем эту логику. class CreateTaskSkill(private val context: Context): CustomSkill<AimyboxRequest, AimyboxResponse> {
override fun canHandle(response: AimyboxResponse) = response.action == "createTask" override suspend fun onResponse( response: AimyboxResponse, aimybox: Aimybox, defaultHandler: suspend (Response) -> Unit ) { val intent = Intent(context, TaskFormActivity::class.java) val additionalData = HashMap<String, Any>() val type = response.intent additionalData["viewed task type"] = when (type) { "habit" -> Task.TYPE_HABIT "daily" -> Task.TYPE_DAILY "todo" -> Task.TYPE_TODO "reward" -> Task.TYPE_REWARD else -> "" } // Inserted code for voice activation
textEditText.setText(bundle.getString("activity_name")) // presetting task name notesEditText.setText(bundle.getString("activity_description")) //presetting task description if (bundle.getBoolean("sentiment")) { // presetting task sentiment habitScoringButtons.isPositive = true habitScoringButtons.isNegative = false } else { habitScoringButtons.isNegative = true habitScoringButtons.isPositive = false } when (bundle.getString("activity_difficulty").toString()) { // presetting task difficulty "trivial" -> taskDifficultyButtons.selectedDifficulty = 0.1f "easy" -> taskDifficultyButtons.selectedDifficulty = 1f "medium" -> taskDifficultyButtons.selectedDifficulty = 1.5f "hard" -> taskDifficultyButtons.selectedDifficulty = 2f else -> taskDifficultyButtons.selectedDifficulty = 1f } Не забываем в data прописать данные, которые мы будем обрабатывать на клиентской стороне – habit, pattern и так далее.Так как название и описание может быть любым, создадим сущности Name и Description, в которой пропишем регулярное выражение, матчащее любое слово. Пока что у нас в названии и описании будет по одному слову. Делаем интент: Указываем, что нам обязательно нужен task_type и сложность. Можем добавить в обязательные и название, и описание – тогда, если пользователь не скажет одно или другое, бот уточнит у него с помощью вопроса слот, который еще не указан. Прописываем разные вариации того, как можно задать название и описание вместе с типом (порядок, отсутствие одного или другого). Тут нет предела совершенству, но для минимума достаточно шаблонов выше.Также для примера здесь я использую язык шаблонов, который можно изменить по нажатию на кнопку слева от ввода. @ – шаблоны и регулярки, “ – примеры и семантическая близость. Теперь сценарий в JAICF. state("createTask") {
activators { intent("createTask") } action { val taskType = activator.getCailaSlot("taskType").asJsonLiteralOr("") reactions.say("Перехожу...") reactions.aimybox?.response?.action = "createTask" reactions.aimybox?.response?.intent = taskType.content reactions.aimybox?.response?.run { data["taskName"] = activator.getCailaSlot("taskName").asJsonLiteralOr("") data["taskDescription"] = activator.getCailaSlot("taskDescription").asJsonLiteralOr("") data["taskSentiment"] = activator.getCailaSlotBool("taskSentiment").asJsonLiteralOr(true) data["taskDifficulty"] = activator.getCailaSlot("taskDifficulty").asJsonLiteralOr("easy") } } } private fun ActivatorContext.getCailaRequiredSlot(k: String): String = getCailaSlot(k) ?: error("Missing Caila slot for key: $k") private fun ActivatorContext.getCailaSlot(k: String): String? = caila?.slots?.get(k) private fun ActivatorContext.getCailaSlotBool(k: String): Boolean? = caila?.slots?.get(k)?.toBoolean() private fun String?.asJsonLiteralOr(other: String) = this?.let { JsonLiteral(this) } ?: JsonLiteral(other) private fun Boolean?.asJsonLiteralOr(other: Boolean) = this?.let { JsonLiteral(this) } ?: JsonLiteral(other) Ссылка на репозиторий с навыком JAICF. Ссылка на репозиторий с кодом Aimybox. =========== Источник: habr.com =========== Похожие новости:
Блог компании Just AI ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_razrabotka_pod_android ( Разработка под Android ), #_kotlin, #_golosovye_interfejsy ( Голосовые интерфейсы ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:38
Часовой пояс: UTC + 5