Android Notifications. Оповещения через Status Bar

Добрый день, хабровчане. Давно занимаюсь разработкой под Android и хотелось бы рассказать сообществу о правильном подходе к созданию уведомлений.

image

На хабре уже есть статья по уведомлениям в статус баре для андроид . В ней рассматриваются основы отображения стандартного и конфигурируемого layout в статус баре.

Ниже, помимо описанного ранее, мы рассмотрим добавление прогрессбара, обработку события по нажатию на уведомлений, различные варианты состояний уведомлений. Рассмотрим добавленный на днях в Compatibility library Notification.Builder. А также поговорим о рекомендациям по UI (design guidlines), которые гугл рекомендует соблюдать при создании уведомлений.


Guidlines
Как советуют разработчики Android в официальном гайдлайне
Когда показывать уведомления:

  • Мы показываем уведомления, когда не хотим отвлекать пользователя, перекрывая ему экран нашими диалогами или переходом на экран уведомления. Мы не отвлекаем пользователя, но при этом не лишаем его возможности узнать содержание нашего уведомления в любой момент.
  • Чаще всего уведомления не всплывают спонтанно, а появляются в моменты, когда пользователь ожидает реакции от приложения.
  • В первую очередь уведомления должны отражать события, зависящие от времени. Как то: события календаря, входящие сообщения, запросы из социальных сетей.


Когда не стоит показывать уведомления:

  1. Не нужно показывать уведомления для не важных псевдо-зависящих от времени событий. Например, новости из социальных сетей.
  2. Нет необходимости показывать то, что уже отображено в UI приложения.
  3. Не стоит отображать ход низкоуровневых операций, вроде обращения к БД.
  4. Если приложение быстро само исправляет ошибку, то не нужно вовсе показывать эту ошибку, тем более уведомлением.
  5. Не показывайте уведомления о сервисах, которые пользователь не может контролировать.
  6. Плохим подходом является создание большого числа уведомлений, с целью напоминать пользователю о приложении, показывая постоянно его иконку и имя.


Хорошая практика:

  1. По клику на уведомление, пользователю должен открываться соответствующий экран приложения. В некоторых случаях достаточно, чтобы по клику уведомление просто убиралось.
  2. Указание времени события в уведомлении, также является хорошим подходом.
  3. Рекомендуется схожие события складывать в одно уведомление, а не отображать на каждое событие своё.
  4. Всегда убирать из статус-бара уведомления, с которыми пользователь уже ознакомился и произвел соответствующие действия.
  5. Показывать маленькое превью уведомления при его создании в свёрнутом статус-бареimage
  6. Позволять пользователю отключать уведомления в настройках приложения.
  7. Использовать иконки, обозначающие принадлежность уведомления определённому приложению. Иконки делать монохромными. Для этого рекомндуется воспользоваться специальным онлайн-редактором
  8. В случае, если событие требует непосредственной реакции пользователя — вместо уведомлений использовать диалоги.


Архитектура:

В качестве утилитки, отвечающей за уведомления, я в своих приложениях использую singleton, к которому можно обратиться из любого класса приложения, нужно лишь иметь ссылку на context.

В ней всегда хранятся ссылки на все созданные во время работы приложения уведомления, которые ещё отображены в статус-баре.

А для присвоения новому уведомлению уникального id используется нехитрый механизм обращения к приватному целочисленному полю, которое каждый раз увеличивается на единицу.
public class NotificationUtils {

  private static final String TAG = NotificationUtils.class.getSimpleName();
     
  private static NotificationUtils instance;

  private static Context context;
  private NotificationManager manager; // Системная утилита, упарляющая уведомлениями
  private int lastId = 0; //постоянно увеличивающееся поле, уникальный номер каждого уведомления
  private HashMap<Integer, Notification> notifications; //массив ключ-значение на все отображаемые пользователю уведомления
 
 
  //приватный контструктор для Singleton
  private NotificationUtils(Context context){
    this.context = context;
    manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    notifications = new HashMap<Integer, Notification>();
  }

/**
* Получение ссылки на синглтон
*/

  public static NotificationUtils getInstance(Context context){
    if(instance==null){
        instance = new NotificationUtils(context);
    } else{
        instance.context = context;
    }
    return instance;
  }

* This source code was highlighted with Source Code Highlighter.


Создание уведомления с помощью NotificationCompat.Builder:
Для того чтобы воспользоваться классами, входящими в библиотеку поддержки прошлых версий (Compatibility library), нужно добавить в проект библиотеку из папки /extras/android/support/v4/android-support-v4.jar
Если же проект нацелен на Android 3.0 и выше, то добавлять ничего не нужно достаточно обратиться к Notification.Builder

public int createInfoNotification(String message){
    Intent notificationIntent = new Intent(context, HomeActivity.class); // по клику на уведомлении откроется HomeActivity
    NotificationCompat.Builder nb = new NotificationCompat.Builder(context)
//NotificationCompat.Builder nb = new NotificationBuilder(context) //для версии Android > 3.0
        .setSmallIcon(R.drawable.ic_action_picture) //иконка уведомления
        .setAutoCancel(true) //уведомление закроется по клику на него
        .setTicker(message) //текст, который отобразится вверху статус-бара при создании уведомления
        .setContentText(message) // Основной текст уведомления
        .setContentIntent(PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT))
        .setWhen(System.currentTimeMillis()) //отображаемое время уведомления
        .setContentTitle("AppName") //заголовок уведомления
        .setDefaults(Notification.DEFAULT_ALL); // звук, вибро и диодный индикатор выставляются по умолчанию

        Notification notification = nb.getNotification(); //генерируем уведомление
        manager.notify(lastId, notification); // отображаем его пользователю.
      notifications.put(lastId, notification); //теперь мы можем обращаться к нему по id
    return lastId++;
  }


* This source code was highlighted with Source Code Highlighter.


Создание уведомления с произвольным отображением (Custom layout):

/**
  * Создание уведомления с прогрессбаром о загрузке
  * @param fileName - текст, отображённый в заголовке уведомления.
  */
  public int createDownloadNotification(String fileName){
    String text = context.getString(R.string.notification_downloading).concat(" ").concat(fileName); //текст уведомления
    RemoteViews contentView = createProgressNotification(text, context.getString(R.string.notification_downloading)); //View уведомления
    contentView.setImageViewResource(R.id.notification_download_layout_image, R.drawable.ic_stat_example); // иконка уведомления
    return lastId++; //увеличиваем id, которое будет соответствовать следующему уведомлению
   }

/**
  * генерация уведомления с ProgressBar, иконкой и заголовком
  *
  * @param text заголовок уведомления
  * @param topMessage сообщение, уотображаемое в закрытом статус-баре при появлении уведомления
  * @return View уведомления.
  */
  private RemoteViews createProgressNotification(String text, String topMessage) {
    Notification notification = new Notification(R.drawable.ic_stat_example, topMessage, System.currentTimeMillis());
    RemoteViews contentView = new RemoteViews(context.getPackageName(), R.layout.notification_download_layout);
    contentView.setProgressBar(R.id.notification_download_layout_progressbar, 100, 0, false);    
    contentView.setTextViewText(R.id.notification_download_layout_title, text);
   
    notification.contentView = contentView;
    notification.flags = Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT | Notification.FLAG_ONLY_ALERT_ONCE;

    Intent notificationIntent = new Intent(context, NotificationUtils.class);
    PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
    notification.contentIntent = contentIntent;
   
    manager.notify(lastId, notification);
    notifications.put(lastId, notification);
    return contentView;
  }


notification_download_layout.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="65sp"
  android:padding="10dp"
  android:orientation="vertical" >

  <LinearLayout
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal" >

    <ImageView
      android:id="@+id/notification_download_layout_image"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:src="@drawable/ic_stat_example"
      android:layout_gravity="center_vertical" />

    <TextView
      android:id="@+id/notification_download_layout_title"
      style="@style/NotificationTitle"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentTop="true"
      android:layout_marginLeft="10dip"
      android:singleLine="true"
      android:text="notification_download_layout_title"
      android:layout_gravity="center_vertical" />
  </LinearLayout>

  <ProgressBar
    android:id="@+id/notification_download_layout_progressbar"
    style="?android:attr/progressBarStyleHorizontal"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="4dp"
    android:progress="0" />

</LinearLayout>


в андроид 2.3 и выше ( API >10) был создан специальный ресурс, в котором системная тема указывает цвета текста уведомений. Из-за этого в старых версиях приходится использовать костыль:

В файл res/values/styles.xml прописываем:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <style name="NotificationText">
   <item name="android:textColor">?android:attr/textColorPrimary</item>
  </style>
  <style name="NotificationTitle">
   <item name="android:textColor">?android:attr/textColorPrimary</item>
   <item name="android:textStyle">bold</item>
  </style>
 
</resources>


А для поддержки API >10 Создаем файл res/values-v9/styles.xml и вписываем:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <style name="NotificationText" parent="android:TextAppearance.StatusBar.EventContent" />
  <style name="NotificationTitle" parent="android:TextAppearance.StatusBar.EventContent.Title" />
</resources>


Теперь из кода нашего приложения обращаемся к утилите:

    NotificationUtils n = NotificationUtils.getInstance(getActivity());
    n.createInfoNotification("info notification");



Создаем уведомление с прогресс-баром:

int pbId = NotificationUtils.getInstance(getActivity()).createDownloadNotification("downloading video");


И во время выполнения потока постоянно обновляем прогресс вызовом:

NotificationUtils.getInstance(getActivity()).updateProgress(pbId, YOUR_PROGRESS);


В итоге получаем:
image

Как видно — нижнее уведомление, созданное нами при помощи билдера может быть удалено в любой момент. А уведомление с прогресс-баром размещается в верхнем блоке уведомлений, в котором пользователь не может очистить уведомления.

И напоследок маленькая хитрость:

Если не хотите дублирования в стеке одних и тех же Activity — поставьте в манифесте к нужной activity
android:launchMode="singleTop"
Поделиться публикацией

Комментарии 25

    0
    Действительно все подробно, но такой вопрос, а в ICS ни как нельзя добавить уведомлению цифру? Notification.number не работает… Может есть какой хак?
    и ещё в том же ICS если обновлять уведомления, то оно всплывает, в предыдущих версиях если время уведомления не менять, то оно всплывает… Тоже как обойти?
      +1
      По поводу номера уведомления: code.google.com/p/android/issues/detail?id=21477
      Если уведомление создано с помощью билдера, то достаточно вопользоваться методом setNumber()
      +7
      синглтон с контекстом?? нет пути! скрепя сердцем поставил вам минус, хотя статья сам по себе хорошая, и пишите вы хорошо.

      нельзя хранить статичные ссылки на активити, оно может быть разрушено в любой момент. тысячу раз эта тема поднималась уже. если так хочется синглтон с контекстом — юзайте сервисы. а вообще в вашем случае можно контекст как параметр передавать, либо как приватное поле этот класс инстанцировать.
        0
        быстрофикс «скрепя сердце»
          0
          Фишка в том, что мы каждый раз при вызове getInstance(Context context) передаем контект вызывающей активити. А следовательно, пока жива активити, в которой мы получили ссылку на инстанс — мы и используем этот контекст. А потом, когда начинаем в другой активити использовать NotificatonUtil — прередаем соответсвующий этой активити контекст.
          Так то считаю притензию необоснованной
            0
            передавать-то передаём, но сохраняем его только в случае, когда instance==null. если же мы вызовем getInstance второй и более раз из другого активити, instance будет неравен null и будет использовать предыдущий контекст
              0
              исправил. даже если instance!=null обновляю ссылку на контекст. Теперь точно контекст будет всегда тот.
                0
                не хочу казаться буквоедом, но компилятор вроде не пропустит this.context
                юзайте instance.context
                  +1
                  Вы безусловно правы, спасибо
            +1
            Хотел о том же написать.

            Я после одного проекта, где меня попросили лики убрать, стараюсь не использовать синглтон в Андроиде, потому что ненароком можно чего и сохранить. И вообще статические переменные нужно использовать с очень большой осторожностью и по возможности их избегать.

            Если уже сильно хочется синглтон — передавать контекст приложения (getApplicationContext()), а в своем синглтоне проверять, что передаешь.
              +1
              Можно еще WeakReference использовать, но тогда надо проверять есть ли активити. На мой взгляд это более правильный путь, чем даже сервис держать.
                0
                а я с этим толком не разобрался ещё. вы бы не могли в двух словах объяснить, как на основе WeakReference синглтон сделать?
                  +1
                  Я имел в виду синглтон создавать как обычно, но контекст хранить WeakReference, тогда при смерте активити gc уберет контекст и все остальное.
                  Т.е. просто вместо поля
                  private Context context
                  написать
                  private WeakReference context
                    0
                    а когда активити разрушится, context будет равен null, так? как туда запихнуть новое значение? в getInstance проверку делать?
                      +1
                      Да. Там придется проверить, если контекст null, то перезаписать его.
                      Зато это гарантированно спасает от того, что при смерти активити мы будем продолжать держать в памяти и использовать уже неактуальный контекст.
                        +1
                        ага, я понял вашу мысль, спасибо)
                0
                Таки путь есть — Application Context является очень даже синглтоном. В статье же использован Base Context, да еще и в статик-поле хранится — это недопустимо.
                  0
                  А особенно это недопустимо для человека, который заявляет что
                  Давно занимаюсь разработкой под Android
                    0
                    Странное дело, но в статье:
                    — в первом методе создания уведомления оно создается с помощью билдера, но при этом в конце используется deprecated метод getNotification() вместо build();
                    — метод создания уведомления с произвольным отображением написан с использованием deprecated методов, вместо билдера, как предыдущем методе;
                    — утечки и много попаболи на 100% обеспечены благодаря хранению ссылки на активити в статик-поле;
                    — хранение lastId и notifications в обычных полях практически бесполезно в разрезе перезапуска приложения при активных уведомлениях, а notifications в статье вообще никак не используется;
                    — со styles.xml напутано;
                    — android:launchMode=«singleTop» может привести к неожиданным последствиям, его назначение не раскрыто полностью, хотя это и не цель статьи;
                    — * This source code was highlighted with Source Code Highlighter — с какого сайта копипейстилось?
                    Все это как-то не вяжется с заявлением
                    Давно занимаюсь разработкой под Android
                    Боюсь данная статья больше вреда принесет неопытным разработчикам, чем пользы. Автор явно поспешил с ее публикацией.
                  0
                  Я очень надеялся увидеть как
                  схожие события складывать в одно уведомление, а не отображать на каждое событие своё.
                    0
                    по полученному id мы можем обратиться к нашему notification и чтонибуть в нем обновить
                    Notification notification = notifications.get(notificationId);
                    notification.setLatestEventInfo(context, contentTitle, contentText, contentIntent);
                    manager.notify(notificationId, notifications.get(notificationId));
                    +1
                    Думаю стоит обосновать, почему «одиночка» в данном случае вообще не подходит. Дело в том что жизненный цикл создаваемых уведомлений привязан к конкретному контексту. Поэтому сущность, отвечающая за показ или скрытие уведомлений должна жить в рамках одного контекста. Ваш класс не отвечает на вопрос: что будет с уведомлением после смерти контекста.

                    По хорошему должен быть один экземпляр, привязанный к активити или к сервису. Скорее всего вам не понадобится создание уведомлений для «дочерних» активити, следовательно разумно привязать экземпляр к классу приложения.
                      0
                      > А для поддержки API >10 Создаем файл res/values-v9/styles.xml и вписываем:

                      Так все-таки, в каком апи оно было введено, 9 или 10 или 11? :)
                        0
                        Таки да, ошибка тут по 10, но вот про values-v9 — указано верно. Т.е. до 9 апи — юзать стили по-умолчанию, а до 9-го апи — это 2.2/Froyo и ниже, смысла создавать проекты с нижним СДК лвл 8 нет уже давно, таких девайсов в обиходе крайне мало осталось. Т.е. если нижний СДК лвл >=9, то это вообще не требуется, в общие стили сразу внести те, что в values-v9 и все.
                          0
                          Пока вы отвечали, рынок и правда изменился :) Оффтопик — API 9 в дикой природе почти не встречается, поэтому если ставить 9, то уже можно сразу 10. А еще лучше фиксировать старый APK и прыгать выше

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое