Полный список
— создаем простой виджет
— разбираемся в его Lifecycle
Добрались таки и до этой долгожданной темы. Вообще, интересно было бы глянуть статистику, какое количество из всех существующих приложений содержит в себе виджеты. Подозреваю, что не чаще, чем одно из десяти. Возможно, ближайшие несколько уроков эту статистику изменят )
Для понимания темы виджетов желательно знать, что такое BroadcastReceiver. Я о нем упоминал в Уроках 96 и 100. Это просто слушатель, который регистрируется в системе, ловит с помощью настроенного Intent Filter сообщения (Intent) и выполняет какой-либо код. Почему-то не сделал я отдельного урока по нему. Но, думаю, еще сделаю. Есть там свои интересные особенности, о которых можно поговорить.
Думаю, нет особой необходимости подробно объяснять, что такое виджеты. Все их видели на своих девайсах. Это что-то типа мини-приложений расположенных на рабочем столе (Home). Они позволяют просмотреть какую-либо информацию из основных приложений, либо повлиять на поведение этих приложений. В качестве примера можно привести – прогноз погоды, текущее время, баланс какого-либо счета, список сообщений в различных мессенджерах, управление состоянием WiFi/3G/GPS/Bluetooth, яркость экрана и т.д. и т.п. В этом уроке сделаем простейший виджет, который отобразит статичный текст.
Чтобы создать простейший виджет нам понадобятся три детали:
В нем мы формируем внешний вид виджета. Все аналогично layout-файлам для Activity и фрагментов, только набор доступных компонентов здесь ограничен следующим списком:
2) XML-файл c метаданными
В нем задаются различные характеристики виджета. Мы пока что укажем следующие параметры:
— layout-файл (из п.1.), чтобы виджет знал, как он будет выглядеть
— размер виджета, чтобы виджет знал, сколько места он должен занять на экране
— интервал обновления, чтобы система знала, как часто ей надо будет обновлять виджет
3) Класс, наследующий AppWidgetProvider. В этом классе нам надо будет реализовать Lifecycle методы виджета.
Давайте создадим три этих детали. Activity нам не понадобится, поэтому не забудьте убрать галку Create Activity в визарде создания нового проекта
Создадим проект без Activity:
Project name: P1171_SimpleWidget
Build Target: Android 2.3.3
Application name: SimpleWidget
Package name: ru.startandroid.develop.p1171simplewidget
Добавим строки в strings.xml:
Создаем layout-файл widget.xml:
RelativeLayout, а внутри зеленый TextView с текстом по центру. Т.е. виджет просто будет показывать текст на зеленом фоне.
Создаем файл метаданных res/xml/widget_metadata.xml:
В атрибуте initialLayout указываем layout-файл для виджета.
Атрибуты minHeight и minWidth содержат минимальные размеры виджета по высоте и ширине.
Есть определенный алгоритм расчета этих цифр. Как вы наверняка замечали, при размещении виджета, экран делится на ячейки, и виджет занимает одну или несколько из этих ячеек по ширине и высоте. Чтобы конвертнуть ячейки в dp, используется формула 70 * n – 30, где n – это количество ячеек. Т.е. если мы, например, хотим, чтобы наш виджет занимал 2 ячейки в ширину и 1 в высоту, мы высчитываем ширину = 70 * 2 – 30 = 110 и высоту = 70 * 1 – 30 = 40. Эти полученные значения и будем использовать в атрибутах minWidth и minHeight.
Атрибут updatePeriodMillis содержит количество миллисекунд. Это интервал обновления виджета. Насколько я понял хелп, указать мы тут можем хоть 5 секунд, но чаще, чем раз в 30 минут (1 800 000) виджет обновляться все равно не будет — это системное ограничение. Давайте пока что поставим интервал 40 минут (2 400 000). В следующих уроках мы разберемся, как самим обновлять виджет с необходимым интервалом.
Осталось создать класс, наследующий AppWidgetProvider.
onEnabled вызывается системой при создании первого экземпляра виджета (мы ведь можем добавить в Home несколько экземпляров одного и того же виджета).
onUpdate вызывается при обновлении виджета. На вход, кроме контекста, метод получает объект AppWidgetManager и список ID экземпляров виджетов, которые обновляются. Именно этот метод обычно содержит код, который обновляет содержимое виджета. Для этого нам нужен будет AppWidgetManager, который мы получаем на вход.
onDeleted вызывается при удалении каждого экземпляра виджета. На вход, кроме контекста, метод получает список ID экземпляров виджетов, которые удаляются.
onDisabled вызывается при удалении последнего экземпляра виджета.
Во всех методах выводим в лог одноименный текст и список ID для onUpdate и onDeleted.
Повторюсь — в onUpdate мы, по идее, должны накодить какое-то обновление виджета. Т.е. если наш виджет отображает, например, текущее время, то при очередном обновлении (вызове onUpdate) надо получать текущее время и передавать эту инфу в виджет для отображения. В этом уроке мы пока не будем с этим возиться.
Осталось немного подрисовать манифест. Добавьте туда ваш класс как Receiver
— укажите для него свои label и icon. Этот текст и эту иконку вы увидите в списке выбираемых виджетов, когда будете добавлять виджет на экран.
— настройте для него фильтр с action = android.appwidget.action.APPWIDGET_UPDATE
— добавьте метаданные с именем android.appwidget.provider и указанием файла метаданных xml/widget_metadata.xml в качестве ресурса
После этого секция receiver в манифесте должна получиться примерно такая:
Виджет готов. Все сохраняем и запускаем. Никаких Activity, разумеется, не всплывет. В консоли должен появиться текст:
\P1171_SimpleWidget\bin\P1171_SimpleWidget.apk installed on device
Done!
Открываем диалог создания виджета и видим в списке наш виджет с иконкой и текстом, которые мы указывали в манифесте для receiver.
Выбираем его и добавляем на экран.
Виджет появился, смотрим логи.
onEnabled
onUpdate [8]
Сработал onEnabled, т.к. мы добавили первый экземпляр виджета. И сразу после добавления, сработал метод onUpdate для этого экземпляра. Видим, что ему назначен (У вас, скорее всего, будет другой ID). Т.е. система добавила экземпляр виджета на экран и вызвала метод обновления с указанием ID экземпляра.
Добавим еще один экземпляр
onEnabled не сработал, т.к. добавляемый экземпляр виджета уже не первый. onUpdate же снова отработал для нового добавленного экземпляра и получил на вход >
Теперь давайте удалим с экрана два этих экземпляра виджета. Сначала второй. В логах увидим:
Сработал onDeleted и получил на вход ID удаляемого экземпляра виджета.
Удаляем первый экземпляр. В логах:
onDeleted [8]
onDisabled
Снова сработал onDeleted — нас оповестили, что экземпляр виджета с был удален. И сработал onDisabled, т.е. был удален последний экземпляр виджета, больше работающих экземпляров не осталось.
Наш виджет обновляется (получает вызов метода onUpdate) раз в 40 минут. Если кому не лень, добавьте снова пару виджетов на экран и подождите. Когда они снова обновятся, в логах это отразится.
Так создается и работает простой виджет. Пока остановимся на этом, чтобы уложились в голове схема создания виджета и его lifecycle-методы. Из интересного хотелось бы еще отметить пару вещей.
BroadcastReceiver
Класс AppWidgetProvider является расширением класса BroadcastReceiver (в манифесте мы его и прописали как Receiver). Он просто получает от системы сообщение в onReceive, определяет по значениям из Intent, какое именно событие произошло (добавление, удаление или обновление виджета), и вызывает соответствующий метод (onEnabled, onUpdate и пр.).
В манифесте мы для нашего Receiver-класса настроили фильтр с action, который ловит события update. Каким же образом этот Receiver ловит остальные события (например, удаление)? Хелп пишет об этом так:
Т.е. ACTION_APPWIDGET_UPDATE – это единственный action, который необходимо прописать явно. Остальные события AppWidgetManager каким-то образом сам доставит до нашего AppWidgetProvider-наследника.
Отступы
Если мы расположим рядом несколько экземпляров виджета, увидим следующую картину
Не очень приятное зрелище. Надо бы сделать отступ.
Добавим android:padding=»8dp» к RelativeLayout в нашем layout-файле
Виджеты на экране поменялись автоматически и теперь выглядят более пристойно.
Кстати, для них сработал onUpdate, смотрите логи. В метод был передан массив ID всех работающих экземпляров виджета.
В Android 4.0 (API Level 14) и выше этот недостаток с отступами был устранен, и вручную делать отступы больше не надо. Давайте проверим. Уберите ранее добавленный в RelativeLayout отступ. И укажите в манифесте android:targetSdkVersion версию 14 (или выше), чтобы система знала, что можно использовать стандартные возможности, а не режим совместимости.
Все сохраняем, запускаем наш виджет на эмуляторе с 4.1. Добавим три экземпляра.
Система сама делает отступы между виджетами.
Получается для версий, ниже 4 надо делать отступ в layout, а для старших версий не надо. Хелп дает подсказку, как сделать так, чтобы ваш виджет корректно работал на всех версиях. Для этого используются квалификаторы версий.
В layout для RelativeLayuot указываете:
И создаете два файла.
res/values/dimens.xml с записью:
и res/values-v14/dimens.xml с записью:
В манифесте android:targetSdkVersion должен быть 14 или выше.
Таким образом, на старых версиях (без системного отступа) отступ будет 8dp, а на новых – 0dp и останется только системный отступ.
На следующем уроке:
— настраиваем виджет при размещении
— работаем с view-компонентами виджета при обновлении
Присоединяйтесь к нам в Telegram:
— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование
— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня
— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме
Источник
Виджеты на Android. Редкая фича, в которой придется разобраться
Привет, Хабр! Меня зовут Александр Хакимов, я android-разработчик в компании FINCH.
У вас бывало такое, что ваш дизайн был под iOS, а вам приходится адаптировать его под android? Если да, то часто ли ваши дизайнеры используют виджеты? К сожалению, виджет — редкий кейс для многих разработчиков, потому что с ним редко кто работает,
В статье я подробно расскажу как создать виджет, на что стоит обратить внимание и поделюсь своим кейсом.
Создание виджета
Для создания виджета нужно знать:
- Особенности компонентов виджета.
- Особенности отображения виджета в сетке экрана.
- Особенности обновления виджета.
Разберем каждый пункт отдельно.
Особенности компонентов виджета
С этим пунктом знаком любой разработчик, который хоть раз работал с RemoteViews. Если вы из таких, смело переходите к следующему пункту.
RemoteViews предназначен для описания и управления иерархиями Views, которые принадлежат процессу другого приложения. С помощью управления иерархиями можно изменять свойства или вызывать методы, принадлежащие View, которое выступает частью другого приложения. В RemoteViews входит ограниченный набор компонентов стандартной библиотеки компонентов android.widget.
View внутри виджетов работают в отдельном процессе (как правило, это домашний экран), поэтому для изменения UI виджета используется расширение BroadcastReceiver — AppWidgetProvider, работающий в нашем приложении.
Особенности отображения виджета в «сетке» экрана
Each widget must define a minWidth and minHeight, indicating the minimum amount of space it should consume by default. When users add a widget to their Home screen, it will generally occupy more than the minimum width and height you specify. Android Home screens offer users a grid of available spaces into which they can place widgets and icons. This grid can vary by a device; for example, many handsets offer a 4×4 grid, and tablets can offer a larger, 8×7 grid.
Переводя на русский: каждый виджет должен задать свои минимальную ширину и высоту, чтобы обозначить минимальное пространство, которое будет им занято по умолчанию.
Пример настроек виджета при создании в Android Studio
Виджет, который добавили на на Home screen, обычно займет больше места чем минимальные ширина и высота экрана, которые вы задали. Android Home screens предоставляет пользователям сетку доступного пространств, в которых могут быть расположены виджеты и иконки. Эта сетка может отличаться в зависимости от устройства; например, многие телефоны предлагают сетку 4х4, а планшеты могут предложить большие сетки 8х7.
Из этого становится понятно, что сетка устройства может быть какой угодно, а размер ячеек может отличаться, в зависимости от размера сетки. Соответственно, контент виджета должен быть спроектирован с учетом этих особенностей.
Минимальные ширину и высоту виджета для заданного количества столбцов и строк можно вычислить по формуле:
minSideSizeDp = 70 × n − 30, где n —количество строк или столбцов
На текущий момент максимальный минимум сетки, которую вы можете задать это 4х4. Это гарантирует, что ваш виджет будет отображаться на всех девайсах.
Особенности обновления виджета
Так как AppWidgetProvider по своей сути является расширением BroadcastReceiver-а, с ним можно делать все то же самое, что и с обычным BroadcastReceiver. AppWidgetProvider просто парсит соответствующие поля из Intent, полученного в onReceive и вызывает методы перехвата с полученными extras.
Сложность возникла с частотой обновления контента — все дело в разнице внутренней работы виджетов на iOS и Android. Дело в том, что данные на iOS-виджетах обновляются тогда, когда виджет становится виден пользователю. В Android, такого события не существует. Мы не можем узнать, когда пользователь видит виджет.
Для виджетов на Android рекомендуемым способом обновления является обновление по таймеру. Настройки таймера задаются параметром виджета updatePeriodMillis. К сожалению, эта настройка не позволяет обновлять виджет чаще чем раз в 30 минут. Ниже я расскажу об этом подробнее.
Кейс создания виджета
Дальше речь пойдет о кейсе который был у нас в FINCH в крупном лотерейном приложении с приложением «Столото» для участия в государственных лотереях.
Задача приложения – упростить и сделать прозрачным для пользователя выбор лотереи и покупку билета. Поэтому требуемая функциональность виджета довольно проста: показывать пользователю рекомендуемые игры для покупки и по тапу переходить к соответствующей. Список игр определяется на сервере и регулярно обновляется.
В нашем кейсе дизайн виджета предусматривал два состояния:
- Для авторизованного пользователя
- Для неавторизованного пользователя
Авторизованному пользователю нужно показывать данные его профиля: состояние внутреннего кошелька, количество билетов ожидающих розыгрыша и сумму неполученных выигрышей. Для каждого из этих элементов предусмотрен, отличный от других, переход на экран внутри приложения.
Как вы могли заметить, еще одной особенностью для авторизованного пользователя является кнопка «обновить», но об этом позже.
Для реализации отображения двух состояний, с учетом дизайна, я использовал RemoteAdapter в виде реализации RemoteViewsService, чтобы генерировать карточки контента.
А теперь немного кода и того, как все работает внутри. Если у вас уже был опыт работы с виджетом, то вы знаете, что любое обновление данных виджета начинается с метода onUpdate:
Пишем апдейт для каждого инстанса нашего виджета.
Пишем реализацию нашего сервиса. В нем нам важно указать, какую реализацию интерфейса RemoteViewsService.RemoteViewsFactory использовать, чтобы генерировать контент.
Фактически это тонкий wrapper над Adapter. Благодаря ему, мы можем связывать наши данные с remote collection view. RemoteViewsFactory предоставляет методы генерации RemoteViews для каждого элемента в наборе данных. У конструктора нет никаких требований — все что я делаю, это передаю в нем контекст.
Далее будет пару слов об основных методах:
- onCreate – создание адаптера.
- getLoadingView – метод предлагает возвращать View, которое система будет показывать вместо пунктов списка, пока они создаются. Если ничего здесь не создавать, то система использует некое дефолтное View.
- getViewAt – метод предлагает создать пункты списка. Здесь идет стандартное использование RemoteViews.
- onDataSetChanged вызывается, когда поступил запрос на обновление данных в списке. Т.е. в этом методе мы подготавливаем данные для списка. Метод заточен под выполнение тяжелого, долгого кода.
- onDestroy вызывается при удалении последнего списка, который использовал адаптер (один адаптер может использоваться несколькими списками).
- RemoteViewsFactory живет пока все инстансы списка живы, поэтому мы можем хранить в нем текущие данные, например, список текущих айтемов.
Определяем список данных, который будем показывать:
При создании адаптера начинаем загружать данные. Здесь спокойно можно выполнять любые тяжелые задачи, в том числе спокойно ходить в сеть блокируя поток.
При вызове команды на обновление данных, так же вызываем updateDataSync()
Внутри updateDataSync тоже все просто. Очищаем текущий список item-ов. Загружаем данные профиля и игры.
Здесь уже поинтереснее
Так как нам важно показывать профиль только авторизованному пользователю, то и информацию профиля нам нужно загружать только в этом случае:
Модель WidgetProfile собирается из разных источников, поэтому логика их получения и её дефолтные значения устроены таким образом, что отрицательное значение кошелька говорит о некорректных данных или проблемах с их получением.
Для бизнес логики отсутствие данных кошелька является критичным, поэтому, в случае некорректного кошелька, модель профиля не будет создана и добавлена в список item-ов.
Метод updateGamesSync() использует getWidgetGamesInteractor и добавляет в список widgetItems набор актуальных для виджета игр.
Прежде чем перейти к генерации карточек, рассмотрим подробнее модель WidgetItem. Она реализована через kotlin sealed class, что делает модель более гибкой, а работу с ней более удобной.
Создаем RemoteViews и определяем их отклик через FillInIntent
Метод setOnClickFillInIntent назначает указанной viewId intent, который будет объединен с родительским PendingIntent для определения поведения при клике на view с этим viewId. Таким образом, мы сможем реагировать на клики пользователей в нашем WidgetProvider.
Ручное обновление виджета
Для нашего виджета было установлено время обновления в полчаса. Можно обновлять его чаще, например, через танцы с WorkManager, но зачем грузить вашу сеть и аккумулятор? Такое поведение на первых этапах разработки казалось адекватным.
Все изменилось когда «бизнес» обратил внимание, что когда пользователь смотрит на виджет, на нем отображаются неактуальные данные: «Вот на моем iPhone, я открываю виджет и там САМЫЕ свежие данные моего профиля».
Ситуация банальна: iOS генерирует новые карточки при КАЖДОМ показе виджетов, ведь для этого у них отведен специальный экран, а Android не имеет подобных событий для виджета в принципе. Пришлось учесть, что некоторые лотереи проводятся раз в 15 минут, поэтому виджет должен давать актуальную информацию – ты хочешь поучаствовать в каком-то тираже, а он уже прошел.
Чтобы выйти из этой неприятной ситуации и как то решить проблему с обновлением данных, мной было предложено и реализовано проверенное временем решение — кнопка «обновить».
Добавляем эту кнопку в макет layout-a со списком и инициализируем её поведение при вызове updateWidget
Первые наработки показали грустную картину: от нажатия на кнопку «обновить» до фактического обновления, могло пройти несколько секунд. Хотя виджет и генерируется нашим приложением, он фактически находится во власти системы и общается с нашим приложением через broadcast-ы.
Т.е. при нажатии на кнопку «обновить» нашего виджета запускается цепочка:
- Получить Intent в onReceive провайдера action’ .
- AppWidgetManager.ACTION_APPWIDGET_UPDATE.
- Вызов onUpdate для всех указанных в intent-e widgetIds.
- Зайти в сеть за новыми данными.
- Обновить локальные данные и отобразить новые карточки списка.
В результате, обновление виджета выглядело не очень красиво, так как нажав на кнопку, мы пару секунд смотрели на тот же виджет. Было непонятно обновились ли данные. Как решить проблему визуального отклика?
Во-первых, я добавил флаг isWidgetLoading с глобальным доступом через интерактор. Роль этого параметра довольно проста — не показывать кнопку «обновить», пока идет загрузка данных виджета.
Во вторых, процесс загрузки данных в фабрике я разделил на три этапа:
START — начало загрузки. На этом этапе состояние всех вьюшек адаптера и глобального флага загрузки меняется на «загружается».
MIDDLE — этап основной загрузки данных. После их загрузки глобальный флаг загрузки переводится в состояние «загружено», а в адаптере отображаются загруженные данные.
END — конец загрузки. Адаптеру на этом шаге не требуется изменять данные адаптера. Этот шаг нужен чтобы корректно обработать этап обновления вьюшек в WidgetProvider.
Давайте посмотрим подробнее как теперь выглядит обновление кнопки в провайдере:
А теперь посмотрим на то, что происходит в адаптере:
- В конце этапов START и MIDDLE я вызываю метод updateWidgets для того, чтобы обновить состояние view управляемых провайдером.
- После выполнения шага START для пользователя визуально отображается «загрузка» в ячейках виджета, и начнется этап MIDDLE.
- Перед тем как вызвать обновление данных адаптера на шаге MIDDLE, провайдер скроет кнопку «обновить».
- После выполнения шага MIDDLE, для пользователя будет отображаются новые данные и начнется этап END.
- Перед тем как вызвать обновление данных адаптера, на шаге END, провайдер скроет кнопку «обновить». С точки зрения фабрики все данные будут актуальными, поэтому на шаге END меняем значение loadingStep на START.
С помощью подобной реализации я достиг компромисс между требованием «бизнеса» видеть на виджете актуальные данные и необходимостью «дергать» обновление слишком часто.
Надеюсь, что статья была для вас полезной. Если у вас был опыт создания виджетов для Android, то расскажите об этом в комментариях.
Источник