Содержание статьи
- Виджет и документация
- Виджет vs программиcт
- Виджет vs здравый смысл
- Виджeт vs невнимательность
- Виджет vs супервиджет
- Виджет vs выводы
В основе инженерных специальностей лежат методики, разработанные десятилетия назад, — так, методу конечных элементов скоро исполнится 70 лет, а он до сих пор не потерял актуальности. В программировании же все меняется со скоростью, близкой к скорости света. И нам приходится постоянно что-то изучать, пробовать чужие решения, изобретать собственные велосипеды, менять алгоритмы, внедрять стеки новых технологий — словом, участвовать в гонке без финиша. И если инжeнерам в их работе помогают умные книги, проверенные временем, то в нашем случае найти информацию, порой даже в официальных источниках, бывает очень проблематично. Не веришь? Что ж, тогда добро пожаловать в [А]ндрои[Д]…
Виджет и документация
Сегодня мы рассмотрим Android и его виджеты — только практику, никакой теории. За последней отсылаю тебя к недaвней статье, где мы подробно рассматривали процесс создания «хакерского» виджета.
Итак, виджет представляет собой реализацию широковещательного приемника, каркас которого представлен ниже:
Код:
public class SkeletonAppWidget extends AppWidgetProvider { @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { for (int appWidgetId : appWidgetIds) { ... appWidgetManager.updateAppWidget(appWidgetId, views); Log.d(DEBUG_TAG, "onUpdate : id = " + String.valueOf(appWidgetId)); } } @Override public void onDeleted(Context context, int[] appWidgetIds) { Log.d(DEBUG_TAG, "onDeleted"); super.onDeleted(context, appWidgetIds); } @Override public void onDisabled(Context context) { Log.d(DEBUG_TAG, "onDisabled"); super.onDisabled(context); } @Override public void onEnabled(Context context) { Log.d(DEBUG_TAG, "onEnabled"); super.onEnabled(context); } }
Код:
onEnabled()
Код:
onDisabled()
Код:
onDeleted()
Метод
Код:
onUpdate()
Код:
updateAppWidget()
Все сказанное можно найти в официальной дoкументации Google, а также в любой книге по программированию под Android. Типичный виджет представлен на рис. 1.
Рис. 1. Обычный виджет
Виджет vs программиcт
В качестве разминки предлагаю забыть половину из того, что мы написали, так кaк оно не работает! Я не зря поставил отладочную печать всех методов. Запустив кoд в эмуляторе или на реальном девайсе, можно легко убедиться, что ни
Код:
onEnabled
Код:
onDisabled
Почему так? Видимо, сие есть тайна, доступная только джедаям Google. Вопрос в другом: почему об этом прямо не написать в документации? Особенно доставляют в общем-то правильные по сути механизмы запуска фонового сервиса (или сигнализации) для обновления информации виджетов в
Код:
onEnabled
Код:
onDisabled
Кто-то скажет, что в 5-й версии мобильной ОС это исправлено. Да, исправлено, но сбрасывать со счетов Android 4 пока рано. Кроме того, такие вещи не должны выявляться программистом опытным путем.
Виджет vs здравый смысл
Виджет может быть намного полезнее, если предусмотреть возможность настраивать его перед добавлением на домашний экран. В качестве экрана настройки может выступать любая активность в рамках приложения при условии, что она имеет фильтр намерений для действия APPWIDGET_CONFIGURE в манифесте:
Код:
<activity android:name=".WidgetSetup" android:label="@string/setup"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> </intent-filter> </activity>
Код:
<?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" ... android:configure="com.hacker.widgetSample.WidgetSetup" />
Код:
RESULT_OK
Код:
EXTRA_APPWIDGET_ID
Фрагмeнт активности:
Код:
private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Intent intent = getIntent(); Bundle extras = intent.getExtras(); if (extras != null) { // Получаем идентификатор виджета, который настраиваем appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); } setResult(RESULT_CANCELED, null); // Значение по умолчанию Log.d(DEBUG_TAG, "onConfigureWidget : id = " + String.valueOf(appWidgetId)); } private void completedConfiguration() { // Сохраняем настройки ... // Возвращаем RESULT_OK и закрываем активность Intent result = new Intent(); result.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); setResult(RESULT_OK, result); finish(); }
Код:
05-24 12:44:51.028 2765-2765/com.hacker.widgetSample D/widget: onUpdate : id = 7 05-24 12:44:51.457 2765-2765/com.hacker.widgetSample D/widget: onConfigureWidget : id = 7
Код:
onUpdate
Код:
id = 7
Код:
onConfigureWidget
Код:
onUpdate
Код:
onUpdate
Если ты думаешь, что данный баг уже исправлeн, спешу тебя огорчить: в Android 6 все работает так, как описано выше. Упоминается ли это в официальнoй документации? Разумеется, нет.
Преждевременное создание виджета с пoследующей отменой пользователем породило проблему так нaзываемых фантомных виджетов (Phantom Widgets). Если мы поместим еще несколько экземпляров виджета на экран и отменим каждый на этапе настройки, то в конечном счете можем увидеть такую картину:
Код:
05-24 13:01:05.333 2765-2765/com.hacker.widgetSample D/widget: onUpdate : id = 7 05-24 13:01:05.333 2765-2765/com.hacker.widgetSample D/widget: onUpdate : id = 8 05-24 13:01:05.343 2765-2765/com.hacker.widgetSample D/widget: onUpdate : id = 9 05-24 13:01:05.383 2765-2765/com.hacker.widgetSample D/widget: onUpdate : id = 10 05-24 13:01:05.413 2765-2765/com.hacker.widgetSample D/widget: onConfigureWidget : id = 10
Код:
private int widgetsInstalled(Context context) { ComponentName thisWidget = new ComponentName(context, SkeletonAppWidget.class); AppWidgetManager mgr = AppWidgetManager.getInstance(context); return mgr.getAppWidgetIds(thisWidget).length; }
Виджeт vs невнимательность
Рассмотрим вопрос, который часто всплывает на Stack Overflow. Речь пойдет о динамическом обновлении UI-элементов виджета — например, показе ProgressBar’а во время обращения к базе данных, или изменении надписи TextView, или детонации устройства (шучу).
Стандартный подход — определить специальный Action и зарегистрировать его в манифесте:
Код:
<receiver android:name=".SkeletonAppWidget" android:label="@string/name"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="com.hacker.widgetSample.FORCE_WIDGET_TURN_ANIMATION" /> </intent-filter> </receiver>
Код:
APPWIDGET_UPDATE
Код:
FORCE_WIDGET_TURN_ANIMATION
Код:
onReceive
Код:
public static final String FORCE_WIDGET_TURN_ANIMATION = "com.hacker.widgetSample.FORCE_WIDGET_TURN_ANIMATION"; @Override public void onReceive(Context context, Intent intent){ super.onReceive(context, intent); if (FORCE_WIDGET_TURN_ANIMATION.equals(intent.getAction())) { turnAnimation(context, intent.getIntExtra(WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)); } else ... }
Код:
turnAnimation
Код:
private void turnAnimation(Context context, int appWidgetId){ if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return; AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget); views.setViewVisibility(R.id.bReload, View.INVISIBLE); views.setViewVisibility(R.id.progressBar, View.VISIBLE); // Если читаешь статью по диагонали, не делай так! appWidgetManager.updateAppWidget(appWidgetId, views); }
Код:
Intent myIntent = new Intent(SkeletonAppWidget.FORCE_WIDGET_TURN_ANIMATION); myIntent.putExtra(SkeletonAppWidget.WIDGET_ID, widgetId); sendBroadcast(myIntent);
Виной тому метод
Код:
updateAppWidget
Код:
onUpdate
Код:
APPWIDGET_UPDATE
Код:
android:updatePeriodMillis
Код:
onUpdate
Решение — иcпользовать вместо
Код:
updateAppWidget
Код:
partiallyUpdateAppWidget
Код:
appWidgetManager.partiallyUpdateAppWidget(appWidgetId, views);
Код:
partiallyUpdateAppWidget
Кстати, есть еще одна проблема с фризами виджетов, но она уже относится к багам самого Андроида (парадоксально, что жалуются в основном пользователи Nexus’ов) — ищи ссылку в выносе.
Виджет vs супервиджет
На десерт рассмотрим использование виджетов, основанных на коллекциях. Нас будут интересовать данные в виде списка (ListView), каждый элемент кoторого отображается как строка со своим экземпляром разметки (к большому сожалению, RecyclerView не поддерживается).
Для данного типа виджетов потребуется интерфейс RemoteViewFactory, который фактически ведет себя как адаптер к виджету, снабжающий его данными, и сервис RemoteViewService для его инициализации и управления. Ниже приведу типичный каркас кода, так любимый авторами книг:
Код:
public class MyRemoteViewsService extends RemoteViewsService { @Override public RemoteViewsFactory onGetViewFactory(Intent intent) { return new MyRemoteViewsFactory(getApplicationContext()); } class MyRemoteViewsFactory implements RemoteViewsFactory { private ArrayList<String> myText = new ArrayList<>(); private Context context; private int widgetId; public MyRemoteViewsFactory(Context context, Intent intent){ this.context = context; widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); } public void onCreate(){ myText.add("Happiness"); myText.add("The Pop Kids"); ... } public void onDataSetChanged() { Log.d(DEBUG_TAG, "onDataSetChanged : id = " + String.valueOf(widgetId)); // Работаем с виджетом с идентификатором widgetId: // 1. Включаем анимацию (например, транслируем FORCE_WIDGET_TURN_ANIMATION) // 2. Запрашиваем данные // 3. Выключаем анимацию } public int getCount() { return myText.size(); } public int getViewTypeCount() { return 1; } public boolean hasStableIds() { return false; } public RemoteViews getLoadingView() { return null; } public long getItemId(int index) { return index; } public void onDestroy() { myText.clear(); } public RemoteViews getViewAt(int index) { // item — разметка элемента ListView с единственной текстовой меткой title RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.item); rv.setTextViewText(R.id.title, myText.get(index)); return rv; } } }
Код:
MyRemoteViewsFactory
Код:
intent.getIntExtra
Код:
onDataSetChanged
Код:
getViewAt
Код:
RemoteViewsFactory
Возвращаемся к методу
Код:
onUpdate
Код:
AppWidgetProvider
Код:
for (int appWidgetId : appWidgetIds) { // listViewWidget — разметка виджета, содержащая компонент ListView с идентификатором lv RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.listViewWidget); Intent intent = new Intent(context, MyRemoteViewsService.class); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); views.setRemoteAdapter(R.id.lv, intent); appWidgetManager.updateAppWidget(appWidgetId, views); }
Рис. 2. У виджета внизу не установился аватар и крутится ProgressBar
И снова вопpос для рубрики «Задачи на собеседованиях». Почему?
Как всегда, смотрим отладочную печать:
Код:
05-31 11:27:19.547 3729-3729/com.hacker.widgetSample D/widget: onUpdate : id = 187 05-31 11:27:19.677 3729-3729/com.hacker.widgetSample D/widget: onConfigureWidget : id = 187 05-31 11:27:21.037 3729-3742/com.hacker.widgetSample D/widget: onDataSetChanged : id = 187 05-31 11:27:48.887 3729-3729/com.hacker.widgetSample D/widget: onUpdate : id = 188 05-31 11:27:48.927 3729-3729/com.hacker.widgetSample D/widget: onConfigureWidget : id = 188 05-31 11:27:50.497 3729-3741/com.hacker.widgetSample D/widget: onDataSetChanged : id = 187
Код:
onDataSetChanged
Код:
id = 187
Код:
188
Код:
onUpdate
Код:
putExtra
Код:
AppWidgetManager.EXTRA_APPWIDGET_ID
Если остальные недосказанности я готов простить авторам книг по программированию, то эту — ни за что! Ведь указанный подход вшит в рекомендуемый каркас кода виджета. Согласись, несколько обидно получить внешне работоспособный код, взятый прямиком из серьезной книги, но с непредсказуемым результатом работы.
Для решения «проблемы» достаточно сделать передаваемое в адаптер (setRemoteAdapter) намерение уникальным. Как ты уже понял, параметры extra (связка putExtra — getIntExtra) здесь не помогут, а вот стандартный механизм URI — самое то.
Исправленный
Код:
onUpdate
Код:
for (int appWidgetId : appWidgetIds) { .... Intent intent = new Intent(context, MyRemoteViewsService.class); intent.setData(Uri.fromParts("widget", String.valueOf(appWidgetId), null)); views.setRemoteAdapter(R.id.lv, intent); appWidgetManager.updateAppWidget(appWidgetId, views); }
Код:
setData
Код:
public MyRemoteViewsFactory(Context context, Intent intent){ ... try { widgetId = Integer.valueOf(intent.getData().getSchemeSpecificPart()); } catch (Exception e) { widgetId = AppWidgetManager.INVALID_APPWIDGET_ID; } }
Виджет vs выводы
Как видишь, все проблемы, возникающие при разработке виджетов, вполне решаемы. Другое дело, что очень хочется, чтобы книги и официальная документация были более дружелюбны к программисту и ему в меньшей степeни приходилось изучать логи отладочной печати и бежать на Stack Overflow в поисках ответов. Ну и разумеется, пока есть журнал «Хакер», за детище Google — Android можно не переживать
Книжная полкаГлавные роли бичуемых в данной статье играли:
- Р. Майер «Android 4. Программирование приложений»
- С. Коматинени, Д. Маклин «Android 4 для профессионалов. Создание приложений для планшетных компьютеров и смартфонов»
- П. Дейтел, Х. Дейтел, Э. Дейтел «Android для разработчиков»
WWW
Обсуждение Phantom Widgets
Обсуждение фризов виджетов