Приложение "Погода". Сервисы. Уведомления.

Урок 34 — Приложение "Погода". Сервисы. Уведомления.

Один из важнейших компонентов Android — Service.

Понятие Service

Сервис — это компонент, который может выполняться "в фоне", то есть независимо от того, находится ли сейчас приложение на экране.

Сервисы делятся на два типа:

  • Запущенный. Сервис, который был запущен другим компонентом приложения. Такой сервис в общем случае работает независимо от других компонентов и, как правило, живёт дольше них. Служба может остановить сама себя, либо может быть уничтожена системой, если приложение будет закрыто окончательно (кроме одного исключения, о котором позже), либо если системе будет мало памяти.
  • Привязанный. Сервис, к которому привязан другой компонент приложения (например, Activity). После привязки компонент может обмениваться данными с сервисом. Как только все компоненты отвяжутся от сервиса, он будет остановлен.

Оба этих типа могут использоваться одновременно.

Обратите внимание: несмотря на то, что сервисы работают независимо от остальных компонентов, они выполняют всю работу на основном потоке. Поэтому если вам нужно выполнить "тяжелую" задачу, например, скачать что-то из сети, придётся создать фоновый поток.

Что ж, давайте попробуем создать сервис на практике и посмотрим, как это работает.

Жизненный цикл сервиса

Точно так же, как и любой другой компонент Android-приложения, Service имеет свой жизненный цикл. Он несколько отличается у запущенного и привязанного сервисов:

Жизненный цикл запущенного сервиса
Жизненный цикл запущенного сервиса

Жизненный цикл привязанного сервиса
Жизненный цикл привязанного сервиса

  • onCreate() — вызывается, когда сервис создаётся
  • onStartCommand() — вызывается у запущенного сервиса, когда он был запущен другим компонентом. Может быть вызван несколько раз за время жизни сервиса.
  • onDestroy() — вызывается, когда сервис уничтожается. Так же, как и в случае с Activity, вызов этого метода не гарантируется.
  • onBind() — вызывается, когда компонент привязывается к сервису.
  • onUnbind() — вызывается, когда компонент отвязывается от сервиса.
  • onRebind() — вызывается, когда компонент привязывается к сервису после того, как был вызван onUnbind().

Запущенный сервис

Создайте новый класс, наследующийся от Service:

public class WeatherService extends Service {

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

}

Как и любой компонент приложения, сервис должен быть зарегистрирован в манифесте внутри тега <application>:

<service android:name=".WeatherService" />

Тег <service> поддерживает следующие атрибуты:

  • android:description — описание для пользователя.
  • android:directBootAware — поддерживает ли сервис Direct Boot (защищённый режим; вам вряд ли потребуется). По умолчанию false.
  • android:enabled — включен ли сервис. По умолчанию true.
  • android:exported — могут ли другие приложения взаимодействовать с сервисом.
  • android:icon — иконка сервиса.
  • android:isolatedProcess — должен ли сервис быть запущен в отдельном изолированном процессе. По умолчанию false.
  • android:label — имя сервиса для пользователя.
  • android:name — имя класса, который содержит реализацию сервиса.
  • android:permission — разрешение, которое должно быть у приложения для запуска сервиса или привязки к нему.
  • android:process — имя процесса, в котором будет работать сервис. По умолчанию используется процесс приложения.

Итак, мы создали сервис. Теперь переопределим метод onStartCommand():

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    Log.i(TAG, "onStartCommand");

    return START_STICKY;
}

Что за START_STICKY?

В методе onStartCommand() мы должны вернуть константу, говорящую системе, что делать, если сервис будет убит. Варианты таковы:

  • START_STICKY — сервис будет перезапущен системой. При этом первоначальный Intent не будет передан, вместо него будет null.
  • START_REDELIVER_INTENT — сервис будет перезапущен, при этом в onStartCommand() будет передан первоначальный Intent.
  • START_NOT_STICKY — сервис не будет автоматически перезапущен.

Обратите внимание: если сервис будет убит из-за необработанного исключения, он будет пересоздан лишь несколько раз. Если он продолжит "падать", система больше не перезапустит его.

Теперь запустим сервис. В конце MainActivity.onCreate() добавьте следующие строки:

Intent intent = new Intent(this, WeatherService.class);
startService(intent);

Запустите приложение. В логах вы увидите:

11-16 02:08:11.673 4210-4210/? I/WeatherService: onStartCommand

Ок, оно работает.

Давайте добавим немного функциональности в наш сервис. Пускай он считает от 0 до переданного числа, увеличивая счётчик раз в секунду.

Добавьте следующие поля в WeatherService:

public static final String EXTRA_COUNT_TO = "count_to";

private int countTo;
private int currentNumber = 0;

Добавьте метод:

private void startCount() {
    final Handler handler = new Handler();

    handler.post(new Runnable() {
        @Override
        public void run() {

            currentNumber++;

            Log.i(TAG, "Current number: " + currentNumber);

            if (currentNumber < countTo) {
                handler.postDelayed(this, 1000);
            }

        }
    });
}

И измените метод onStartCommand() следующим образом:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    countTo = intent.getIntExtra(EXTRA_COUNT_TO, 0);

    startCount();

    return START_REDELIVER_INTENT;
}

В MainActivity измените код запуска сервиса:

Intent intent = new Intent(this, WeatherService.class);
intent.putExtra(WeatherService.EXTRA_COUNT_TO, 5);

startService(intent);

Теперь запустите приложение и посмотрите в логи:

11-16 02:15:06.942 4768-4768/com.skillberg.weather2 I/WeatherService: Current number: 1
11-16 02:15:07.944 4768-4768/com.skillberg.weather2 I/WeatherService: Current number: 2
11-16 02:15:08.951 4768-4768/com.skillberg.weather2 I/WeatherService: Current number: 3
11-16 02:15:09.962 4768-4768/com.skillberg.weather2 I/WeatherService: Current number: 4
11-16 02:15:10.974 4768-4768/com.skillberg.weather2 I/WeatherService: Current number: 5

А теперь попробуйте убить приложение, посмотрите в логи ещё раз, и с сожалением наблюдайте за тем, как сервис начал считать заново — приложение было убито, и система перезапустила сервис :(

Как сделать так, чтобы сервис был максимально "живучим", и не умирал вместе с приложением? Нужно создать так называемый Foreground Service.

Foreground Service отличается от обычного тем, что у него минимум шансов быть убитым, а визуально он отличается тем, что создаёт уведомление.

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

Уведомления

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

Для уведомления нам потребуется иконка. Создадим её так же, как и создавали иконки ранее, но в типе выберите Notification Icons:

Создаём иконку для уведомлений
Создаём иконку для уведомлений

Создайте в сервисе новый метод:

private void showNotification() {

}

В нём создадим Builder для уведомлений:

NotificationCompat.Builder builder = new NotificationCompat.Builder(this);

Зададим иконку, заголовок и текст уведомления:

builder.setSmallIcon(R.drawable.ic_notification);
builder.setContentTitle("Content title");
builder.setContentText("Content text");

Теперь нужно передать Intent, который будет запускать MainActivity при клике на уведомления:

Intent mainIntent = new Intent(this, MainActivity.class);

Однако, есть проблема: по сути, этот Intent будет запущен другим приложением, поэтому мы не сможем передать его напрямую. Чтобы это заработало, нам нужен класс PendingIntent, он позволяет передать Intent в другое приложение:

PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
                mainIntent, PendingIntent.FLAG_UPDATE_CURRENT);

Теперь зададим этот PendingIntent:

builder.setContentIntent(pendingIntent);

И, наконец, создадим уведомление:

Notification notification = builder.build();

За показ уведомлений отвечает класс NotificationManager. Получим его:

NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

Ну и покажем уведомление:

notificationManager.notify(0, notification);

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

Теперь вызовите метод showNotification() в onStartCommand():

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    countTo = intent.getIntExtra(EXTRA_COUNT_TO, 0);

    showNotification();

    startCount();

    return START_REDELIVER_INTENT;
}

Запустите приложение, и посмотрите, что получилось:

Уведомление в Android
Уведомление в Android

Foreground Service

Теперь мы имеем представление о том, как показывать уведомления. Пора создать Foreground Service!

Переименуйте метод showNotification() в createNotification() и измените его следующим образом:

private Notification createNotification() {
    NotificationCompat.Builder builder = new NotificationCompat.Builder(this);

    builder.setSmallIcon(R.drawable.ic_notification);
    builder.setContentTitle("Counter is running");
    builder.setContentText("Current number: " + currentNumber);
    builder.setOngoing(true);

    Intent mainIntent = new Intent(this, MainActivity.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(
            this,
            0,
            mainIntent,
            PendingIntent.FLAG_UPDATE_CURRENT
    );

    builder.setContentIntent(pendingIntent);

    return builder.build();
}

Как видите, мы добавили currentNumber в текст уведомления, и теперь мы не показываем уведомление через NotificationManager — просто вернём уведомление из этого метода.

Переопределите метод onCreate():

@Override
public void onCreate() {
    super.onCreate();

    Notification notification = createNotification();
    startForeground(1, notification);
}

Когда мы вызываем startForeground(), система самостоятельно показывает уведомление. Первый параметр — идентификатор для уведомления. Идентификатор ни в коем случае не должен быть равен 0!

В метод startCount() добавим код, который будет обновлять уведомление:

private void startCount() {
    final Handler handler = new Handler();

    handler.post(new Runnable() {
        @Override
        public void run() {
            currentNumber++;

            Log.i(TAG, "Current number: " + currentNumber);

            Notification notification = createNotification();
            startForeground(1, notification);

            if (currentNumber < countTo) {
                handler.postDelayed(this, 1000);
            }
        }
    });
}

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

Запустите приложение, и вы увидите, как счётчик обновляется!

Уведомление из Foreground Service
Уведомление из Foreground Service

Кроме того, если вы убьёте приложение, ничего не изменится — сервис продолжит считать!


Продолжение доступно на платных тарифах

Это недорого — всего от 440 ₽ в месяц!


ВЫБРАТЬ ТАРИФ


Продолжение доступно после регистрации

Все уроки на сайте доступны абсолютно бесплатно после регистрации.

Регистрация займёт меньше минуты ;)