Android widget and app in one apk

Code a Widget for Your Android App: Updating the Widget

Application widgets provide your users with easy access to your application’s most frequently used features, while giving your app a presence on the user’s homescreen. By adding a widget to your project, you can provide a better user experience, while encouraging users to remain engaged with your application, as every single time they glance at their homescreen they’ll see your widget, displaying some of your app’s most useful and interesting content.

In this three part series, we’re building an application widget that has all the functionality you’ll find in pretty much every Android application widget.

In the first post, we created a widget that retrieves and displays data, and performs a unique action in response to onClick events. However, our widget is still missing one crucial piece of functionality: it never updates with new information. A widget that never has anything new to offer isn’t all that useful, so we’ll give our widget two different ways to update.

By the end of this series, we’ll have expanded our widget to retrieve and display new data automatically based on a schedule, and in response to user interaction.

We’re picking up right where we left off, so if you don’t have a copy of the widget we created in the first post, then you can download it from GitHub.

Updating Your Layout

The first step is updating our layout to display some data that changes over time. To help us see exactly when our widget receives each update, I’m going to instruct the widget to retrieve and display the current time, whenever it performs an update.

I’m going to add the following to the widget layout:

  • A TextView that displays a Last Update label.
  • A TextView that will display the time of the last update.

While we’re working on the new_app_widget.xml file, I’m also going to add the TextView that’ll eventually allow the user to trigger an update manually:

Next, define the new string resources that we reference in our layout:

The @string/time looks different from your typical string, as it’s just a placeholder that’ll be replaced at runtime.

This gives us the finished layout:

Updating Your Widget Based on a Schedule

Since it’s the easiest method to implement, I’m going to start by updating our widget automatically, after a set period of time has elapsed.

When creating this kind of automatic update schedule, 1800000 milliseconds (30 minutes) is the smallest interval you can use. Even if you set your project’s updatePeriodMillis to less than 30 minutes, you widget will still only update once every half an hour.

Deciding how frequently your widget should update is never straightforward. Frequent updates are a greater drain on the device’s battery, but set these updates too far apart and your widget may wind up displaying significantly out of date information to the user.

To find a balance that works for your particular project, you’ll need to test your widget across a range of update frequencies and measure the impact each frequency has on battery life, while monitoring whether the widget’s content ever becomes noticeably out of date.

Since how-frequent-is-too-frequent is one of those frustrating subjective questions where there’s no “right” answer, it can help to get a second opinion by arranging some user testing. You could even setup some A/B testing to check whether certain update frequencies are received more positively than others.

Open your project’s AppWidgetProviderInfo file (res/xml/new_app_widget_info.xml) and you’ll see that it already defines a default update interval of 86400000 milliseconds (24 hours).

There are plenty of widgets that only update once every 24 hours, for example a widget that displays the weather forecast might only need to retrieve new information once per day. However, even if your widget does only need to update once per day, this isn’t ideal when testing your app. No-one wants to wait 24 hours just to see whether their onUpdate() method is triggering correctly, so you’ll typically use the shortest possible interval when performing initial testing, and then change this value later, if required.

To make it easier to test our app, I’m going to use the smallest possible interval:

Retrieving and Displaying the Current Time

The onUpdate() method is responsible for updating your widget’s Views with new information. If you open your project’s widget provider (java/values/NewAppWidget.java) then you’ll see that it already contains the outline of an onUpdate() method:

Regardless of whether your widget updates automatically or in response to user interaction, the update process is exactly the same: the widget manager sends a broadcast intent with the ACTION_APPWIDGET_UPDATE action, and the widget-provider class responds by calling the onUpdate() method, which in turn calls the updateAppWidget() helper method.

Once we’ve updated our NewAppWidget.java class to retrieve and display the current time, our project will use this same section of code regardless of how the onUpdate() method is triggered:

Читайте также:  Виджет стрелочные часы для андроид

There’s one problem with the above code: if the user taps the TextView multiple times during the span of a minute, there’ll be no visual indication that the widget has updated, simply because there’s no new time to display. This probably won’t cause problems for your end-users, who are unlikely to sit there, tapping the TextView multiple times per minute and wondering why the time hasn’t changed. However, this may be a problem when testing your app, as it means you’ll have to wait at least 60 seconds in between triggering the onUpdate() method.

To ensure there’s always some kind of visual confirmation that the onUpdate() method has ran successfully, I’m going to display a toast whenever onUpdate() is called:

Updating Your Widget in Response to User Action

Our widget will now update automatically once every half an hour, but what about updating in response to user interaction?

We’ve already laid a lot of the groundwork by adding a Tap to Update TextView to our layout and expanding the NewAppWidget.java class to retrieve and display the current time whenever onUpdate() is called.

The only thing left to do is create an Intent with AppWidgetManager.ACTION_APPWIDGET_UPDATE , which is an action that informs the widget that it’s time to update, and then call this Intent in response to the user interacting with the Tap to Update TextView .

Here’s the completed NewAppWidget.java class:

Being able to update a widget on demand can be invaluable when testing your project, as it means you’ll never have to wait for the update interval to elapse. Even if you don’t include this functionality in your finished application, you may want to add a temporary Tap to Update button, just to make your life easier when testing your project.

Creating a Preview Image

Our widget now looks completely different to the widget that Android Studio generated for us automatically, but at the moment it’s still using the auto-generated preview image.

Ideally, your preview image should encourage users to select your widget from the Widget Picker, but at the very least it should be an accurate representation of how your widget actually looks! Currently, our preview image ticks neither of those boxes, so we need to create a new one.

The easiest method, is to use the Widget Preview application that’s included in the Android emulator.

Creating a Widget Preview

First, install your project on an Android Virtual Device (AVD). Then open the AVD’s app drawer and launch the Widget Preview application. The AVD will display a list of every app that’s installed on the device—select your app from the list.

Your widget will be displayed on a blank background. Spend some time resizing and tweaking your widget so that it looks its best, and once you’re happy with the results, select Take Snapshot. The screenshot will be saved as a PNG in the AVD’s Download folder. To retrieve this file, switch back to Android Studio and open the Device File Explorer, by selecting View > Tool Windows > Device File Explorer from the toolbar. In the Device File Explorer view, navigate to the sdcard/Download folder, where you’ll find the preview image saved as: [app_name]_ori_[orientation].png

Drag this image out of Android Studio and drop it somewhere easily accessible, such as your Desktop. Give the image a more descriptive name, then drag and drop the image into your project’s drawable folder. Now you can open the AppWidgetProviderInfo file (in this instance, that’s new_app_widget_info.xml) and change the following line to reference your new preview image:

Finally, you can remove the unnecessary example_appwidget_preview drawable from your project.

Testing Your Widget

It’s time to put the finished widget to the test!

First, install the updated project on your physical Android device or AVD. Remove all existing instances of this widget from your homescreen, so you know you’re working with the latest version.

To add your widget, press any empty space on the homescreen, tap Widget and then select your widget. You’ll get a chance to resize and reposition the widget, as necessary.

The Last Update value will display the time that you created this widget. Press Tap to Update and the widget should display a toast and a new time. If you place a second instance of the widget on your homescreen it should display a different time to the first one.

You can update either of these widget instances, by giving its Tap to Update TextView a tap. If you trigger a manual update for the first widget, then the second widget will not be updated, and vice versa.

In addition to manual updates, the App Widget Manager will update all instances of your widget once every 30 minutes, based on the time when you created the first instance. Although manual updates can result in instances displaying different times, once every half an hour all instances will be updated simultaneously, at which point they’ll display the same time.

You may want to keep a few instances of this widget on your homescreen, so you can monitor how their content changes over time in response to a combination of automatic and manual updates.

Conclusion

In this series, we’ve been creating an Android application widget that demonstrates how to implement all the most common features found in app widgets. If you’ve been following along since the beginning, then by this point you’ll have built a widget that updates automatically and in response to user input, and that’s capable of reacting to onClick events (you can also download the complete project from GitHub).

Читайте также:  Displaying images in android

In the next post, we’ll look at some best practices for ensuring your widget provides a good user experience, and how to enhance your widget with a configuration Activity.

Источник

Виджеты на Android. Редкая фича, в которой придется разобраться

Привет, Хабр! Меня зовут Александр Хакимов, я android-разработчик в компании FINCH.

У вас бывало такое, что ваш дизайн был под iOS, а вам приходится адаптировать его под android? Если да, то часто ли ваши дизайнеры используют виджеты? К сожалению, виджет — редкий кейс для многих разработчиков, потому что с ним редко кто работает,

В статье я подробно расскажу как создать виджет, на что стоит обратить внимание и поделюсь своим кейсом.

Создание виджета

Для создания виджета нужно знать:

  1. Особенности компонентов виджета.
  2. Особенности отображения виджета в сетке экрана.
  3. Особенности обновления виджета.

Разберем каждый пункт отдельно.

Особенности компонентов виджета

С этим пунктом знаком любой разработчик, который хоть раз работал с 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 для каждого элемента в наборе данных. У конструктора нет никаких требований — все что я делаю, это передаю в нем контекст.

Читайте также:  Ассистент трейдера для андроида

Далее будет пару слов об основных методах:

  1. onCreate – создание адаптера.
  2. getLoadingView – метод предлагает возвращать View, которое система будет показывать вместо пунктов списка, пока они создаются. Если ничего здесь не создавать, то система использует некое дефолтное View.
  3. getViewAt – метод предлагает создать пункты списка. Здесь идет стандартное использование RemoteViews.
  4. onDataSetChanged вызывается, когда поступил запрос на обновление данных в списке. Т.е. в этом методе мы подготавливаем данные для списка. Метод заточен под выполнение тяжелого, долгого кода.
  5. onDestroy вызывается при удалении последнего списка, который использовал адаптер (один адаптер может использоваться несколькими списками).
  6. 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-ы.

Т.е. при нажатии на кнопку «обновить» нашего виджета запускается цепочка:

  1. Получить Intent в onReceive провайдера action’ .
  2. AppWidgetManager.ACTION_APPWIDGET_UPDATE.
  3. Вызов onUpdate для всех указанных в intent-e widgetIds.
  4. Зайти в сеть за новыми данными.
  5. Обновить локальные данные и отобразить новые карточки списка.

В результате, обновление виджета выглядело не очень красиво, так как нажав на кнопку, мы пару секунд смотрели на тот же виджет. Было непонятно обновились ли данные. Как решить проблему визуального отклика?

Во-первых, я добавил флаг isWidgetLoading с глобальным доступом через интерактор. Роль этого параметра довольно проста — не показывать кнопку «обновить», пока идет загрузка данных виджета.

Во вторых, процесс загрузки данных в фабрике я разделил на три этапа:

START — начало загрузки. На этом этапе состояние всех вьюшек адаптера и глобального флага загрузки меняется на «загружается».

MIDDLE — этап основной загрузки данных. После их загрузки глобальный флаг загрузки переводится в состояние «загружено», а в адаптере отображаются загруженные данные.

END — конец загрузки. Адаптеру на этом шаге не требуется изменять данные адаптера. Этот шаг нужен чтобы корректно обработать этап обновления вьюшек в WidgetProvider.

Давайте посмотрим подробнее как теперь выглядит обновление кнопки в провайдере:

А теперь посмотрим на то, что происходит в адаптере:

  1. В конце этапов START и MIDDLE я вызываю метод updateWidgets для того, чтобы обновить состояние view управляемых провайдером.
  2. После выполнения шага START для пользователя визуально отображается «загрузка» в ячейках виджета, и начнется этап MIDDLE.
  3. Перед тем как вызвать обновление данных адаптера на шаге MIDDLE, провайдер скроет кнопку «обновить».
  4. После выполнения шага MIDDLE, для пользователя будет отображаются новые данные и начнется этап END.
  5. Перед тем как вызвать обновление данных адаптера, на шаге END, провайдер скроет кнопку «обновить». С точки зрения фабрики все данные будут актуальными, поэтому на шаге END меняем значение loadingStep на START.

С помощью подобной реализации я достиг компромисс между требованием «бизнеса» видеть на виджете актуальные данные и необходимостью «дергать» обновление слишком часто.

Надеюсь, что статья была для вас полезной. Если у вас был опыт создания виджетов для Android, то расскажите об этом в комментариях.

Источник

Оцените статью