Хабрахабр

Она вам не Android. Особенности разработки под Wear OS

Компания опубликовала новые дизайн-гайдлайны и обновила документацию. 18 марта Google переименовала операционную систему для носимой электроники Android Wear и начала распространять её под именем Wear OS, чтобы привлечь новую аудиторию. Поэтому хочу поделиться своим опытом и рассказать подробнее про Wear OS, из чего она состоит и как с ней работать. Когда я начал разработку приложения для часов, не нашел ни одной русскоязычной публикации на эту тему. Всех небезразличных к мобильным технологиям прошу под кат.

0, система научилась работать с «Standalone Apps» – полностью независимыми wearable-приложениями. Начиная с версии Android Wear 2. Wear OS – это практически независимая система, которая всё ещё продолжает работать в рамках инфраструктуры Google Services, дополняя её, но не привязываясь к ней. Пользователь может установить их с нативного Google Play прямо на часы.

Android, но не очень

Поэтому, если вы уже знакомы с Android-разработкой, то сложностей с Wear OS возникнуть не должно. Как бы Google ни позиционировала Wear OS, платформа основана на Android со всеми его особенностями, прелестями и недостатками. Wear OS почти не отличается от своего «старшего брата», за исключением отсутствия некоторых пакетов:

  • android.webkit
  • android.print
  • android.app.backup
  • android.appwidget
  • android.hardware.usb

Но серфить на часах будет всё равно неудобно. Да, браузер на часах мы в ближайшее время не сможем увидеть из-за отсутствия Webkit. Структурных и архитектурных отличий тоже будет мало. У нас по-прежнему есть великий и ужасный Android Framework с Support Library и Google Services.

Структура приложения

Открыли Android Studio, нажали «New project» и поставили галочку напротив «Wear». Предположим, мы решили сделать wearable-приложение. Мы сразу обнаружим, что в пакете нашего приложения появилось два модуля: wear и mobile.

Упрощенная оригинальная схема

Но они должны иметь одно название пакета, и при публикации должны быть подписаны одним релизным сертификатом.
Собираться эти два модуля будут в два разных .apk файла. Мы к этому вернемся чуть позже. Это нужно только для того, чтобы приложения могли друг с другом взаимодействовать через Google Services. В принципе, ничто не мешает нам собрать приложение только на Wear OS, откинув мобильную платформу в сторону.

Clean architecture?

Это такое же Android-приложение, поэтому архитектурные подходы для него могут быть схожие с Android. А почему бы и нет?

Упрощенная оригинальная схема

Я использовал такой же стек технологий, который мы используем в Android-приложениях:

  • Kotlin
  • Clean architecture
  • RxPM (как презентационный паттерн)
  • Koin (для реализации DI)
  • RxJava (просто дело вкуса)

Поэтому часть логики и моделей можно вынести в ещё один модуль «common». У нас два модуля в проекте, и модели данных, скорее всего, будут одинаковые для обеих платформ. Затем подключить его к mobile и wearable пакетам, чтобы не дублировать код.

UI

В Wear OS, ещё и разная форма экрана: круглый, квадратный и круглый с обрезанным краем.
Если мы попробуем сверстать какой-либо лейаут и отобразить его на разных экранах, скорее всего, увидим примерно такой вот кошмар: Одна из главных особенностей Android-разработки – обилие девайсов разного размера и с разным разрешением экрана.

поехавшая верстка

Пробежимся по самым любопытным из них.
Во второй версии системы Google любезно решила часть UI-проблем, включив в Support wearable library новые адаптивные view-компоненты.

BoxInsetLayout

Он помещает их в прямоугольную область, вписанную в окружность экрана. BoxInsetLayout – это FrameLayout, который умеет адаптировать дочерние элементы под круглый дисплей. Для квадратных дисплеев подобные преобразования, само собой, игнорируются.

BoxInsetLayout

Таким образом, одна и та же верстка будет примерно одинаково выглядеть для всех форм экранов часов.

Правильная верстка

Выглядит лучше, не правда ли?

WearableRecyclerView

Wear-интерфейсы исключением не стали. Списки – удобный паттерн, который активно используется в мобильном (и не только) UX. WearableRecyclerView помогает исправить такие недоразумения.
Например, есть параметр isEdgeItemsCenteringEnabled, который позволяет задать компоновку элементов по изгибу экрана и расширять центральный элемент, делает список более удобным для чтения на маленьком экране.
Есть WearableLinearLayoutManager, который позволяет прокручивать список механическим колесиком на часах и доскроливать крайние элементы до середины экрана, что очень удобно на круглых интерфейсах. Но из-за закругления углов дисплея верхние View у списка могут обрезаться.

Wearable RecyclerView

Они все разные, и обо всех можно подробно почитать в документации.
Сейчас библиотека поддержки Wear включает пару десятков адаптивных View.

В случае мобильного клиента, мы чаще используем REST API поверх привычных всем сетевых протоколов (HTTP/TCP). Рисовать данные на экране – весело, но эти данные нужно откуда-то получать. А активное интернет-соединение будет быстро сажать батарею, и могут регулярно происходить разрывы связи. В Wear OS подобный подход тоже допустим, но Google его не рекомендует.
В носимой электронике большую роль играет энергоэффективность. Классы для работы с ним нашли свое место в пакете com.google.android.gms.wearable. Ещё носимые устройства предполагают активную синхронизацию, которую тоже нужно реализовывать.
Все эти проблемы за нас любезно решает механизм обмена данными в Google Services под названием «Data Layer».

Data Layer

Он выбирает наиболее оптимальный маршрут для обмена данными (bluetooth, network) и реализует стабильную передачу. Data Layer помогает синхронизировать данные между всеми носимыми устройствами, привязанными к одному Google аккаунта пользователя. Это гарантирует, что сообщение дойдет до нужного девайса.

Data Layer

Data Layer состоит из пяти основных элементов:

  • Data Items
  • Assets
  • Messages
  • Channels
  • Capabilities

Data Item

Работать с ними можно через Data Client. Data Item – компонент, который предназначен для синхронизации небольших объемов данных между устройствами в wearable-инфраструктуре. Вся синхронизация реализуется через Google сервисы.

DataItem состоит из трёх частей:

  • payload – это полезная нагрузка в 100kb, представленная в виде ByteArray. Это выглядит немного абстрактно, поэтому сами Google рекомендуют класть туда какую-нибудь key-value структуру вроде Bundle или Map<String, Any>.
  • patch – это путь-идентификатор, по которому мы можем опознать наш DataItem. Дело в том, что Data Client хранит все DataItem’ы в линейной структуре, что подходит не для всех кейсов. Если нам надо отразить какую-то иерархию данных, то придется делать это самостоятельно, различая объекты по URI.
  • Assets – это отдельная структура, которая в самом DataItem’е не хранится, но он может иметь ссылку на нее. О ней поговорим позже.

Для этого воспользуемся PutDataRequest, которому передадим все нужные параметры. Давайте попробуем создать и сохранить DataItem. Затем PutDataRequest скормим DataClient’у в метод putDataItem().

С его помощью мы можем работать с данными, как с Bundle-объектом, в который можно сохранять примитивы. Для удобства есть DataMapItem, в котором уже решена проблема сериализации.

val dataClient = Wearable.getDataClient(context)
val dataRequest = PutDataMapRequest.create(PATCH_COFFEE).apply { dataMap.putString(KEY_COFFEE_SPECIEES, "Arabica") dataMap.putString(KEY_COFFEE_TYPE, "Latte") dataMap.putInt(KEY_COFFEE_SPOONS_OF_SUGAR, 2)
}
val putDataRequest = dataRequest.asPutDataRequest()
dataClient.putDataItem(putDataRequest)

Теперь наш DataItem хранится в DataClient’е, и мы можем получить к нему доступ со всех Wearable-девайсов.
Теперь мы можем забрать у DataClient список всех Item’ов, найти тот, который нас интересует, и распарсить его:

dataClient.dataItems.addOnSuccessListener }
}

Assets

DataItem с такой нагрузкой не справится, потому как предназначен для быстрой синхронизации, а вот Asset может. А теперь давайте представим, что нам внезапно потребовалось отправить на часы фотографию, аудио или еще какой-то файл. Возможен сценарий, когда Item сохранился быстрее Asset, а файл всё еще продолжает загружаться. Механизм синхронизации ассетов предназначен для сохранения файлов размером более 100kb в wearable-инфраструктуре и плотно связан с DataClient’ом.
Как упоминалось ранее, DataItem может иметь ссылку на Asset, но сами данные сохраняются отдельно.

Создать Asset можно с помощью Asset.createFrom[Uri/Bytes/Ref/Fd], после чего передать его в DataItem:

val dataClient = Wearable.getDataClient(context)
val dataRequest = PutDataMapRequest.create(PATCH_COFFEE).apply { dataMap.putString(KEY_COFFEE_SPECIES, "Arabica") dataMap.putString(KEY_COFFEE_TYPE, "Latte") dataMap.putInt(KEY_COFFEE_SPOONS_OF_SUGAR, 2) // Добавляем фото val asset = Asset.createFromUri(Uri.parse(COFFEE_PHOTO_PATCH)) dataMap.putAsset(KEY_COFFEE_PHOTO, asset)
}
val putDataRequest = dataRequest.asPutDataRequest()
dataClient.putDataItem(putDataRequest)

Чтобы загрузить Asset на другой стороне, нужно открыть inputStream, получить сам массив байт, а затем представить его в нужной нам форме:

dataClient.dataItems.addOnSuccessListener { dataItems -> dataItems.forEach { item -> if (item.uri.path == PATCH_COFFEE) { val mapItem = DataMapItem.fromDataItem(item) val asset = mapItem.dataMap.getAsset(KEY_COFFEE_PHOTO) val coffee = Coffee( mapItem.dataMap.getString(KEY_COFFEE_SPECIES), mapItem.dataMap.getString(KEY_COFFEE_TYPE), mapItem.dataMap.getInt(KEY_COFFEE_SPOONS_OF_SUGAR), // Сохраняем файл из Asset saveFileFromAsset(asset, COFFEE_PHOTO_PATCH) ) coffeeReceived(coffee) } }
} private fun saveFileFromAsset(asset: Asset, name: String): String { val imageFile = File(context.filesDir, name) if (!imageFile.exists()) { Tasks.await(dataClient.getFdForAsset(asset)).inputStream.use { inputStream -> val bitmap = BitmapFactory.decodeStream(inputStream) bitmap.compress(Bitmap.CompressFormat.JPEG, 100, imageFile.outputStream()) } } return imageFile.absolutePath
}

Capabilities

Представим ситуацию, когда нужно отправить сообщение не на все устройства, а на какие-то конкретные часы. Сеть носимых девайсов может быть гораздо шире, чем два устройства, соединенные по Bluetooth, и включать в себя десятки девайсов. Способ есть – это механизм Capabilities. Нужен способ для идентификации устройств в этой сети. Звучит довольно просто. Смысл его очень прост – любой девайс-участник сети с помощью CapabilitiesClient может узнать, какое множество узлов поддерживает ту или иную функцию, и отправить сообщение именно на один из этих узлов.
Для того чтобы добавить Capabilities в наше wearable-приложение, нужно создать файл res/values/wear.xml и записать туда массив строк, которые и будут обозначать наши Capabilities. На практике тоже ничего сложного:

wear.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources> <string-array name="android_wear_capabilities"> <item>capability_coffee</item> </string-array>
</resources>

На стороне другого устройства:

fun getCoffeeNodes(capabilityReceiver: (nodes: Set<Node>) -> Unit) { val capabilityClient = Wearable.getCapabilityClient(context) capabilityClient .getCapability(CAPABILITY_COFFEE, CapabilityClient.FILTER_REACHABLE) .addOnSuccessListener { nodes -> capabilityReceiver.invoke(nodes.nodes) }
}

Этот объект довольно часто фигурирует во фреймворках от Google (в т.ч. Если у вас, как и у меня, развился Rx головного мозга, то от себя порекомендую расширение для объекта Task. Firebase):

fun <T : Any?> Task<T>.toSingle(fromCompleteListener: Boolean = true): Single<T> { return Single.create<T> { emitter -> if (fromCompleteListener) { addOnCompleteListener { if (it.exception != null) { emitter.onError(it.exception!!) } else { emitter.onSuccess(it.result) } } } else { addOnSuccessListener { emitter.onSuccess(it) } addOnFailureListener { emitter.onError(it) } } }
}

Тогда цепочка для получения Nodes будет выглядеть красивее:

override fun getCoffeeNodes(): Single<Set<Node>> = Wearable.getCapabilityClient(context) .getCapability(CAPABILITY_COFFEE, CapabilityClient.FILTER_REACHABLE) .toSingle() .map { it.nodes }

Messages

Message помогает отправлять сообщения без синхронизации в формате «отправили и заб(ы|и)ли». Все предыдущие компоненты Data Layer предполагали кэширование данных. Причем отправить сообщение можно только на конкретный узел или на конкретное множество узлов, которые предварительно необходимо получить через CapabilitiesClient:

fun sendMessage(message: ByteArray, node: Node) { val messageClient = Wearable.getMessageClient(context) messageClient.sendMessage(node.id, PATCH_COFFEE_MESSAGE, message) .addOnSuccessListener { // Success :) } .addOnFailureListener { // Error :( }
}

Потенциальный получатель сообщения, в свою очередь, должен подписаться на получение сообщений, и найти нужное по его URI:

val messageClient = Wearable.getMessageClient(context)
messageClient.addListener { messageEvent -> if (messageEvent.path == PATCH_COFFEE_MESSAGE) { // TODO: coffee processing }
}

Channels

Например, если нам нужно отправить голосовое сообщение с часов на телефон, то каналы будут очень удобным инструментом. Каналы служат для передачи потоковых данных в режиме реального времени без кэширования. Клиент для каналов можно получить через Wearable.getChannelClient(), и дальше открыть входной или выходной поток данных (один канал может работать в обе стороны).

Google активно развивает Data Layer, и вполне вероятно, что через полгода эти клиенты снова куда-то «переедут», или их API снова поменяется.
Разумеется, Data Layer – не единственный способ общения с внешним миром, никто не запретит нам по-старинке открыть tcp-socket и разрядить устройство пользователя.

В заключение

Wear OS быстро развивается. Это был всего лишь краткий обзор актульных технических возможностей платформы. Support Wearable Library тоже не стоит на месте и меняется вместе с платформой, радуя нас новыми UI-компонентами и чудесами синхронизации.
Как и у любой другой системы, тут есть свои тонкости и интересные моменты, о которых можно говорить долго. Устройств становится больше, и возможно, скоро это будут не только часы. Делитесь своим опытом wearable-разработки в комментариях. Многие детали остались раскрыты не полностью, поэтому пишите в комментариях, о чем хочется поговорить подробнее, и мы расскажем об этом в следующей статье.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть