Базовые компоненты Android: Фундамент разработки

Этот курс подробно рассматривает четыре основных строительных блока любого Android-приложения. Вы изучите принципы работы Activity, Service, Broadcast Receiver и Content Provider, а также механизмы их взаимодействия.

Activity и жизненный цикл: Основы взаимодействия с пользователем

Activity и жизненный цикл: Основы взаимодействия с пользователем

Добро пожаловать в курс «Базовые компоненты Android: Фундамент разработки». Мы начинаем наше погружение в мир мобильной разработки с самого главного, самого заметного и фундаментального компонента любого Android-приложения — Activity (Активити).

Если вы когда-либо открывали приложение на своем смартфоне, вы уже взаимодействовали с Activity. В этой статье мы разберем, что это такое, как оно работает «под капотом» и почему понимание его жизненного цикла отличает профессионального разработчика от новичка.

Что такое Activity?

В самом простом понимании, Activity — это один экран вашего приложения. Это контейнер для пользовательского интерфейса (UI), с которым взаимодействует человек.

Представьте себе приложение «Контакты».

  1. Список всех контактов — это одна Activity.
  2. Экран добавления нового контакта — это вторая Activity.
  3. Экран детальной информации о контакте — это третья Activity.

Хотя они работают вместе, создавая ощущение единого приложения, технически это независимые компоненты, которые запускают друг друга.

Activity — это компонент приложения, который предоставляет экран, с которым пользователи могут взаимодействовать для выполнения чего-либо: набора номера, съемки фото, отправки письма или просмотра карты.

Роль Activity в архитектуре Android

В отличие от программ на языках C или Java, которые обычно начинают выполнение с метода main(), система Android инициализирует код в экземпляре Activity, вызывая специфические методы обратного вызова (callbacks), соответствующие определенным этапам ее жизненного цикла.

Каждая Activity должна быть объявлена в файле манифеста приложения (AndroidManifest.xml). Если вы создадите файл класса Activity, но забудете добавить запись о нем в манифест, система не сможет запустить этот экран и приложение упадет с ошибкой.

Пример объявления в манифесте:

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

Здесь фильтр намерений (intent-filter) с категорией LAUNCHER говорит системе, что именно эта Activity является точкой входа в приложение (появляется при нажатии на иконку в меню телефона).

Жизненный цикл Activity

Понимание жизненного цикла — это «таблица умножения» для Android-разработчика. Activity не просто существует; она рождается, живет, уходит на второй план и умирает. В каждый из этих моментов система вызывает специальные методы, которые вы можете переопределить, чтобы выполнить нужные действия.

Зачем это нужно? Представьте, что пользователь смотрит видео в вашем приложении, а затем ему звонят. Ваше приложение должно:

  1. Приостановить видео.
  2. Освободить ресурсы (например, камеру или тяжелые объекты в памяти).
  3. Сохранить прогресс просмотра.
  4. После завершения звонка — восстановить всё как было.

Без управления жизненным циклом ваше приложение будет потреблять батарею, терять данные пользователя и вылетать.

Основные состояния и методы

Рассмотрим каждый этап подробно, следуя хронологии жизни Activity.

1. onCreate() — Рождение

Это первый метод, который вызывается при создании Activity. Он срабатывает только один раз за весь цикл жизни экземпляра.

Что здесь делать:

  • Инициализировать UI (вызвать setContentView).
  • Создавать переменные и объекты.
  • Привязывать данные к спискам.
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // Устанавливаем макет интерфейса из XML-файла
    setContentView(R.layout.activity_main)
    // Инициализация компонентов
}

Обратите внимание на параметр savedInstanceState. Если Activity пересоздается (например, после поворота экрана), этот объект содержит данные, которые вы сохранили перед уничтожением. Если Activity запускается впервые, он равен null.

2. onStart() — Появление

Метод вызывается, когда Activity становится видимой пользователю. Однако, взаимодействовать с ней пока нельзя. Она как бы «проявляется» на экране.

Что здесь делать:

  • Запускать анимации, которые должны быть видны сразу при появлении.
  • Регистрировать широковещательные приемники (BroadcastReceivers), которые нужны только когда экран виден.
3. onResume() — Активность

Сразу после onStart() вызывается onResume(). В этот момент Activity находится на переднем плане, и пользователь может нажимать кнопки, вводить текст и скроллить. Activity переходит в состояние «Resumed» (Возобновлено).

Важно: Activity остается в этом состоянии до тех пор, пока что-то не переключит фокус (например, входящий звонок, переход на другой экран или выключение экрана).

Что здесь делать:

  • Запускать воспроизведение видео или аудио.
  • Запускать обновление данных с датчиков (GPS, акселерометр).
4. onPause() — Потеря фокуса

Этот метод вызывается, когда пользователь начинает покидать Activity или она перекрывается другим окном (например, полупрозрачным диалогом), но всё ещё частично видна.

Что здесь делать:

  • Остановить анимации или видео, чтобы не тратить процессорное время.
  • Сохранить черновик сообщения, если пользователь начал его писать.
  • Нельзя: Выполнять тяжелые операции (сохранение в базу данных, сетевые запросы), так как это заблокирует переход к следующему экрану.
5. onStop() — Исчезновение

Когда Activity больше не видна пользователю (полностью перекрыта другой Activity или приложение свернуто), вызывается onStop().

Что здесь делать:

  • Освобождать тяжелые ресурсы, которые не нужны, пока пользователь не видит экран.
  • Сохранять данные в базу данных (SQLite, Room) или настройки.
  • Останавливать тяжелые вычисления.
6. onDestroy() — Уничтожение

Последний вызов перед тем, как Activity будет полностью удалена из памяти. Это может произойти, если пользователь нажал кнопку «Назад» (Back) или если система решила убить процесс для освобождения памяти.

Что здесь делать:

  • Окончательная очистка ресурсов (закрытие соединений, отписка от всех событий).
7. onRestart() — Перезапуск

Этот метод вызывается только в одном случае: если Activity была остановлена (onStop()), но не уничтожена, и пользователь снова вернулся к ней. Сразу за ним последует onStart().

Сводная таблица методов

Метод Описание Следующий метод
onCreate() Создание Activity. Инициализация. onStart()
onStart() Activity становится видимой. onResume()
onResume() Activity готова к взаимодействию. onPause()
onPause() Activity теряет фокус, но может быть видна. onStop() или onResume()
onStop() Activity больше не видна. onDestroy() или onRestart()
onRestart() Возврат к Activity после onStop(). onStart()
onDestroy() Полное уничтожение Activity. Нет

Стек Activity (Back Stack)

Android управляет навигацией между экранами с помощью структуры данных, называемой стек (или Back Stack). Это работает по принципу LIFO (Last In, First Out — «последним пришел, первым ушел»).

  1. Когда вы запускаете новую Activity, она кладется поверх текущей в стек и получает фокус.
  2. Предыдущая Activity останавливается (onStop), но сохраняется в стеке.
  3. Когда пользователь нажимает кнопку «Назад», верхняя Activity уничтожается (onDestroy) и выкидывается из стека.
  4. Предыдущая Activity возобновляется (onRestart -> onStart -> onResume) и снова показывается пользователю.

Если пользователь будет нажимать «Назад» до тех пор, пока стек не опустеет, приложение закроется, и пользователь вернется на домашний экран (Launcher).

Проблема поворота экрана

Одна из самых частых проблем новичков — поведение Activity при повороте устройства.

По умолчанию, при изменении конфигурации (поворот экрана, смена языка, подключение клавиатуры) система уничтожает текущую Activity и создает её заново.

Это происходит следующим образом:

  1. onPause()
  2. onStop()
  3. onDestroy()
  4. ...Поворот...
  5. onCreate() (новый экземпляр)
  6. onStart()
  7. onResume()

Почему так сделано? Это позволяет приложению автоматически загрузить альтернативные ресурсы. Например, у вас может быть отдельный макет (layout) для горизонтальной ориентации (layout-land), где кнопки расположены иначе. Система перезапускает Activity, чтобы применить этот новый макет.

Последствия: Если вы хранили какие-то данные в обычных переменных класса Activity (например, счетчик нажатий), они сбросятся к значениям по умолчанию, так как старый объект класса был удален, а новый создан с нуля.

Для решения этой проблемы используются:

  • Метод onSaveInstanceState(outState: Bundle) для сохранения небольших данных.
  • Архитектурный компонент ViewModel (который мы изучим в следующих модулях курса) для сохранения сложных данных.

Практические советы

  1. Всегда вызывайте super: При переопределении любого метода жизненного цикла (например, onCreate), первой строкой всегда должен быть вызов метода родительского класса: super.onCreate(savedInstanceState). Иначе вы получите исключение SuperNotCalledException.

  2. Не блокируйте UI-поток: Методы жизненного цикла выполняются в главном (UI) потоке. Если вы напишете в onCreate код, который скачивает файл из интернета в течение 5 секунд, ваше приложение «зависнет» на 5 секунд, и система предложит пользователю закрыть его (ошибка ANR — Application Not Responding).

  3. Следите за парностью: Если вы зарегистрировали какой-то ресурс в onStart, освободите его в onStop. Если в onResume — освободите в onPause.

Заключение

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

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

Service: Организация фоновой работы и длительных процессов

Service: Организация фоновой работы и длительных процессов

В предыдущей статье мы подробно разобрали Activity — «лицо» вашего приложения. Мы узнали, что Activity неразрывно связана с тем, что пользователь видит на экране. Но что происходит, когда пользователь сворачивает приложение, чтобы ответить на сообщение, или блокирует экран, продолжая слушать музыку?

Если бы музыкальный плеер полагался только на Activity, музыка бы остановилась в момент вызова метода onStop(). Чтобы этого не произошло, в Android существует специальный компонент для работы «за кулисами» — Service (Сервис).

Что такое Service?

Service — это компонент приложения, который может выполнять длительные операции в фоновом режиме и не предоставляет пользовательского интерфейса (UI).

Представьте ресторан. Activity — это официант. Вы видите его, общаетесь с ним, делаете заказ. Service — это повар на кухне. Вы его не видите, но он выполняет тяжелую работу по приготовлению вашего заказа. Официант может уйти к другому столику (Activity может быть свернута), но повар продолжит жарить стейк (Service продолжает работу).

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

Когда использовать Service?

  1. Воспроизведение музыки: Плеер должен играть, пока вы читаете новости в браузере.
  2. Сбор данных с датчиков: Фитнес-трекер записывает GPS-трек пробежки при выключенном экране.
  3. Загрузка файлов: Скачивание большого обновления контента.

Типы сервисов

В Android сервисы делятся на три основных типа, каждый из которых предназначен для своих задач.

1. Foreground Service (Активный сервис)

Выполняет операции, которые заметны пользователю. Такой сервис обязан отображать уведомление (Notification) в строке состояния (шторке), которое пользователь не может смахнуть, пока сервис работает.

Примеры: Музыкальный плеер, навигатор, трекер бега.

Система относится к таким сервисам с высоким приоритетом и не убивает их при нехватке памяти, так как понимает: пользователь знает об этом процессе и он ему нужен прямо сейчас.

2. Background Service (Фоновый сервис)

Выполняет операции, которые не заметны пользователю напрямую.

Примеры: Сжатие фотографий в галерее, синхронизация небольших данных.

Важно: Начиная с Android 8.0 (API 26), система накладывает жесткие ограничения на запуск фоновых сервисов. Если приложение свернуто, система через некоторое время остановит такой сервис для экономии батареи. Для отложенных задач сейчас рекомендуется использовать WorkManager, о котором мы поговорим в будущих статьях.

3. Bound Service (Привязанный сервис)

Сервис, к которому «привязываются» другие компоненты (например, Activity). Это создает клиент-серверный интерфейс внутри приложения. Activity может отправлять запросы сервису, получать ответы и даже осуществлять межпроцессное взаимодействие (IPC).

Сервис живет только до тех пор, пока к нему привязан хотя бы один компонент. Как только все «клиенты» отвязались, сервис уничтожается.

Жизненный цикл Service

У сервиса, как и у Activity, есть свой жизненный цикл, но он значительно проще. Методы, которые вызываются системой, зависят от того, как именно был запущен сервис.

Основные методы

  • onCreate(): Вызывается при первом создании сервиса. Здесь происходит разовая инициализация (настройка переменных). Если сервис уже запущен, этот метод не вызовется повторно.
  • onStartCommand(): Самый важный метод для запущенных сервисов. Вызывается каждый раз, когда какой-либо компонент (например, Activity) вызывает startService(). Именно здесь начинается выполнение задачи.
  • onBind(): Вызывается, когда компонент хочет привязаться к сервису через bindService(). Если вы не планируете привязку, этот метод должен возвращать null.
  • onDestroy(): Вызывается перед тем, как сервис будет уничтожен. Здесь нужно очистить ресурсы (остановить потоки, закрыть файлы).

Возвращаемые значения onStartCommand

Метод onStartCommand должен вернуть целое число, которое скажет системе, как поступить, если она вдруг убьет сервис из-за нехватки памяти:

  1. START_STICKY: «Липкий» запуск. Если система убьет сервис, она позже перезапустит его и снова вызовет onStartCommand (но с пустым интентом). Идеально для музыкальных плееров (сервис восстановится, и музыка продолжит играть или будет готова к игре).
  2. START_NOT_STICKY: Если сервис убит, он не будет перезапущен автоматически, пока не поступит новая команда startService. Подходит для задач, которые не критично завершать (например, синхронизация данных).
  3. START_REDELIVER_INTENT: Похож на START_STICKY, но при перезапуске система передаст последний Интент, который был обработан. Подходит для загрузки файлов (чтобы продолжить скачивание конкретного файла).

Главный миф о Service

Это критически важный момент, на котором ошибаются многие новички.

Service по умолчанию работает в ГЛАВНОМ (UI) потоке приложения.

Слово «Service» не означает «отдельный поток» (Thread). Оно означает «компонент без интерфейса».

Если вы напишете в методе onStartCommand код, который на 10 секунд блокирует выполнение (например, сложный расчет или сетевой запрос), ваше приложение зависнет, и пользователь увидит ошибку ANR (Application Not Responding), даже если Activity в этот момент свернута.

Как делать правильно: Внутри onStartCommand вы должны самостоятельно запустить новый поток (используя Thread, Executor или Kotlin Coroutines) и выполнять тяжелую работу там.

Объявление в манифесте

Как и Activity, любой Service должен быть объявлен в файле AndroidManifest.xml. Без этого система не сможет его найти и запустить.

<manifest ... >
  <application ... >
      <activity android:name=".MainActivity" />

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

  </application>
</manifest>

Пример реализации простого сервиса

Давайте посмотрим, как выглядит скелет простейшего сервиса на Kotlin.

class MySimpleService : Service() {

    override fun onCreate() {
        super.onCreate()
        // Инициализация ресурсов
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // Внимание: этот код выполняется в UI потоке!
        // Для тяжелых задач запускайте корутины или потоки здесь.

        println("Сервис начал работу")

        // Если система убьет сервис, перезапусти его
        return START_STICKY
    }

    override fun onBind(intent: Intent?): IBinder? {
        // Мы не используем привязку, возвращаем null
        return null
    }

    override fun onDestroy() {
        super.onDestroy()
        println("Сервис уничтожен")
        // Очистка ресурсов
    }
}

Запуск этого сервиса из Activity выглядит так:

val intent = Intent(this, MySimpleService::class.java)
startService(intent)

Остановка сервиса (например, по нажатию кнопки «Стоп»):

val intent = Intent(this, MySimpleService::class.java)
stopService(intent)

Также сервис может остановить сам себя, вызвав метод stopSelf() внутри своего кода, когда задача выполнена.

Foreground Service и уведомления

Как мы упоминали, для длительных операций, которые важны пользователю (музыка), нужно использовать Foreground Service.

Для этого внутри onStartCommand нужно вызвать метод startForeground(). Этот метод принимает ID уведомления и само уведомление.

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

Заключение

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

Ключевые выводы:

  1. Service не имеет UI.
  2. Service работает в главном потоке (нужно создавать потоки вручную).
  3. Для длительных операций, о которых пользователь должен знать, используйте Foreground Service с уведомлением.
  4. Не забывайте останавливать сервис (stopSelf или stopService), иначе вы будете зря расходовать батарею пользователя.

В следующей статье мы разберем компонент, который позволяет приложению «слышать» систему и другие приложения — BroadcastReceiver.

Broadcast Receiver: Обработка системных событий и уведомлений

Broadcast Receiver: Обработка системных событий и уведомлений

Мы продолжаем наш курс «Базовые компоненты Android». В прошлых статьях мы научились создавать интерфейс с помощью Activity и выполнять фоновую работу с помощью Service. Теперь представьте ситуацию: ваше приложение — это музыкальный плеер. Пользователь слушает музыку, но вдруг выдергивает наушники. Что должно произойти? Правильно, музыка должна встать на паузу.

Но как ваше приложение узнает об этом событии? Ведь Activity может быть свернута, а Service просто крутит байты аудиофайла. Здесь на сцену выходит третий кит Android-разработки — Broadcast Receiver (Широковещательный приемник).

Что такое Broadcast Receiver?

Broadcast Receiver — это компонент Android, который позволяет приложению получать события (сообщения) от системы или от других приложений. Это «уши» вашей программы.

Работа этого компонента построена на паттерне проектирования Publish-Subscribe (Издатель-Подписчик):

  1. Издатель (Broadcaster): Отправляет сообщение (Intent) в систему. Это может быть сама система Android (сообщает о низком заряде батареи, входящем SMS, включении Wi-Fi) или другое приложение.
  2. Подписчик (Receiver): Настроен на прослушивание конкретных типов сообщений. Когда такое сообщение появляется в эфире, Receiver перехватывает его и выполняет код.

Зачем это нужно?

Без Broadcast Receiver приложение жило бы в вакууме. Вот лишь несколько примеров использования:

  • Смена сети: Приложение для скачивания файлов должно приостановить загрузку, если пропал Wi-Fi, чтобы не тратить мобильный трафик.
  • Загрузка устройства: Будильник должен заново установить таймеры после перезагрузки телефона (ACTION_BOOT_COMPLETED).
  • Батарея: Игра может снизить качество графики, если заряд батареи упал ниже 15%.

Способы регистрации Receiver

Существует два фундаментально разных способа «подписаться» на события. Выбор способа зависит от того, как долго вы хотите слушать эфир.

1. Статическая регистрация (через Manifest)

Вы объявляете приемник в файле AndroidManifest.xml.

Особенность: Такой Receiver работает, даже если ваше приложение полностью закрыто и выгружено из памяти. Если произойдет событие, система сама запустит ваше приложение (создаст процесс) и передаст сообщение приемнику.

Пример объявления в манифесте:

<receiver android:name=".MyReceiver" android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
        <action android:name="android.intent.action.LOCALE_CHANGED"/>
    </intent-filter>
</receiver>

Ограничения (Важно!): Начиная с Android 8.0 (API 26), Google сильно ограничил использование статических ресиверов для большинства системных событий (кроме нескольких исключений, вроде BOOT_COMPLETED). Это сделано для экономии батареи: представьте, если 50 приложений проснутся одновременно только потому, что вы включили Wi-Fi. Телефон бы просто завис.

2. Динамическая регистрация (через Context)

Вы создаете и регистрируете приемник прямо в коде (в Activity или Service).

Особенность: Такой Receiver живет ровно столько, сколько живет компонент, который его создал (или пока вы его не отпишете вручную). Если вы зарегистрировали его в onCreate вашей Activity, а Activity была уничтожена, приемник перестанет работать.

Это рекомендуемый способ для событий, которые нужны только пока пользователь работает с приложением (например, обновление UI при потере интернета).

Пример регистрации в Activity:

class MainActivity : AppCompatActivity() {

    // Создаем экземпляр нашего приемника
    private val airplaneModeReceiver = AirplaneModeReceiver()

    override fun onStart() {
        super.onStart()
        // Определяем, на что подписываемся
        val filter = IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)
        // Регистрируем приемник
        registerReceiver(airplaneModeReceiver, filter)
    }

    override fun onStop() {
        super.onStop()
        // ОБЯЗАТЕЛЬНО отписываемся, чтобы не было утечек памяти
        unregisterReceiver(airplaneModeReceiver)
    }
}

Создание собственного Receiver

Чтобы создать свой приемник, нужно наследоваться от класса BroadcastReceiver и переопределить всего один метод — onReceive().

class AirplaneModeReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        // Этот метод вызывается, когда происходит событие

        if (intent.action == Intent.ACTION_AIRPLANE_MODE_CHANGED) {
            val isTurnedOn = intent.getBooleanExtra("state", false)

            if (isTurnedOn) {
                Toast.makeText(context, "Режим полета включен!", Toast.LENGTH_LONG).show()
            } else {
                Toast.makeText(context, "Режим полета выключен!", Toast.LENGTH_LONG).show()
            }
        }
    }
}

Жизненный цикл и ограничения onReceive

Метод onReceive() — это критическая точка.

  1. Главный поток (Main Thread): По умолчанию onReceive выполняется в главном потоке. Это значит, что здесь нельзя выполнять сетевые запросы, сложные вычисления или долгие операции с базой данных. Если метод будет выполняться дольше 10 секунд, система покажет ошибку ANR (Application Not Responding).
  2. Краткость жизни: Объект BroadcastReceiver активен только во время выполнения метода onReceive. Как только метод завершился, система считает объект неактивным и может его уничтожить.

Что делать, если нужно выполнить долгую работу? Нельзя просто запустить поток внутри onReceive, так как система может убить процесс сразу после выхода из метода.

Правильное решение: использовать WorkManager для планирования задачи или, в редких случаях, запустить Foreground Service (если это разрешено политиками Android).

Пользовательские (Custom) Broadcasts

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

Отправка сообщения:

val intent = Intent("com.example.myapp.ACTION_UPDATE_DATA")
intent.putExtra("data_id", 123)
sendBroadcast(intent)

Приемник должен быть настроен на Action "com.example.myapp.ACTION_UPDATE_DATA".

Совет по безопасности: Если вы отправляете широковещательное сообщение, которое предназначено только для вашего приложения, используйте LocalBroadcastManager (в старых проектах) или, что более современно, паттерны реактивного программирования (Kotlin Flow, LiveData), так как глобальные Broadcasts могут быть перехвачены другими приложениями, что создает дыру в безопасности.

Влияние версий Android (Evolution)

Android постоянно закручивает гайки в отношении фоновой работы, и Broadcast Receiver пострадал от этого больше всех.

  • До Android 7.0: Можно было слушать почти всё через манифест.
  • Android 7.0 (Nougat): Запретили слушать CONNECTIVITY_ACTION (смена сети) через манифест. Теперь только динамически.
  • Android 8.0 (Oreo): Запретили большинство неявных (implicit) широковещательных сообщений в манифесте.
  • Android 9+: Ограничения на доступ к информации о Wi-Fi и звонках.

Это означает, что современный Android-разработчик должен полагаться на WorkManager для отложенных задач, а BroadcastReceiver использовать преимущественно для событий, происходящих, пока приложение активно (динамическая регистрация).

Практические советы

  1. Не забывайте отписываться: Если вы зарегистрировали ресивер в onStart, отпишите его в onStop. Если в onCreate — то в onDestroy. Иначе вы получите утечку памяти (Memory Leak).
  2. Не делайте тяжелую работу: onReceive должен отработать мгновенно. Получили сигнал -> запустили другую сущность для обработки -> вышли.
  3. Экспортируемость: При объявлении в манифесте атрибут android:exported указывает, может ли ваш ресивер принимать сообщения от других приложений. Начиная с Android 12, вы обязаны явно указывать true или false.

Заключение

Broadcast Receiver — это нервная система вашего приложения, реагирующая на раздражители внешнего мира. Несмотря на ограничения в новых версиях Android, он остается незаменимым инструментом для реакции на изменение состояния устройства (зарядка, режим полета, перезагрузка).

Теперь в вашем арсенале есть:

  • Activity для общения с пользователем.
  • Service для работы, пока пользователь занят.
  • Broadcast Receiver для реакции на события системы.

В следующей статье мы разберем последний из четырех китов Android — Content Provider, который позволяет безопасно обмениваться данными между разными приложениями (например, получить доступ к контактам или галерее).

Content Provider: Управление данными и их совместное использование

Content Provider: Управление данными и их совместное использование

Мы подошли к финалу изучения «Большой четверки» компонентов Android. Мы уже знаем, как создавать экраны (Activity), выполнять фоновую работу (Service) и слушать системные события (Broadcast Receiver).

Но остается один фундаментальный вопрос: как приложения обмениваются данными?

В Android действует строгая политика безопасности, называемая «песочницей» (sandbox). Каждое приложение живет в своем изолированном пространстве. Приложение «А» не может просто так залезть в базу данных приложения «Б» или прочитать его файлы. Это сделано для защиты данных пользователя.

Однако, иногда обмен данными необходим. Например, мессенджеру нужно получить список ваших контактов, а галерее — показать фотографии, сделанные камерой. Именно для безопасного, контролируемого доступа к данным и существует четвертый компонент — Content Provider (Поставщик контента).

Что такое Content Provider?

Content Provider — это компонент, который управляет доступом к центральному репозиторию данных. Он выступает в роли посредника между хранилищем данных (базой данных, файловой системой или облаком) и внешним миром.

Представьте себе банк. Деньги (данные) лежат в надежном хранилище. Вы (другое приложение) не можете просто зайти в хранилище и взять деньги. Вы подходите к операционисту в окошке (Content Provider). Вы передаете ему запрос («Хочу снять 100 рублей»), он проверяет ваши права (паспорт/PIN-код), сам идет в хранилище, берет деньги и выдает их вам.

Зачем он нужен?

  1. Безопасность: Провайдер позволяет точно настроить, кто и как может читать или изменять данные. Вы можете разрешить чтение всем, а запись — никому.
  2. Абстракция: Другому приложению не нужно знать, как именно вы храните данные (в базе SQLite, в JSON-файле или скачиваете из сети). Провайдер предоставляет единый стандартный интерфейс для доступа.
  3. Inter-Process Communication (IPC): Это стандартный способ передачи сложных данных между разными процессами в Android.

Как это работает: URI и ContentResolver

Чтобы получить доступ к данным провайдера, клиентское приложение использует объект ContentResolver. Это «клиентская сторона» механизма.

Но как объяснить системе, какие именно данные нам нужны? Для этого используются URI (Uniform Resource Identifier).

Структура Content URI

Каждый набор данных в Content Provider имеет свой уникальный адрес. Он выглядит примерно так:

content://com.example.app.provider/users/1

Разберем его по частям:

  • content:// — Стандартный префикс (схема), указывающий, что это данные под управлением Content Provider.
  • com.example.app.provider — Authority (Полномочия). Уникальное имя провайдера в системе Android. Обычно совпадает с именем пакета приложения.
  • users — Path (Путь). Указывает на тип данных (например, таблицу в базе данных).
  • 1 — ID (Идентификатор). Необязательная часть. Указывает на конкретную запись (строку) в таблице. Если ID нет, значит, мы обращаемся ко всему списку.

Основные операции (CRUD)

Content Provider реализует четыре базовые операции, известные как CRUD (Create, Read, Update, Delete). Все они выполняются через методы ContentResolver.

1. Чтение данных (Query)

Самая частая операция. Она очень похожа на SQL-запрос SELECT. Метод query() возвращает объект Cursor.

// Пример: Получение списка контактов
val contentResolver = context.contentResolver
val uri = ContactsContract.Contacts.CONTENT_URI

// Какие колонки хотим получить (проекция)
val projection = arrayOf(
    ContactsContract.Contacts.DISPLAY_NAME,
    ContactsContract.Contacts._ID
)

// Условие выборки (WHERE)
val selection = "${ContactsContract.Contacts.HAS_PHONE_NUMBER} = ?"
val selectionArgs = arrayOf("1") // Только контакты с номером телефона

// Сортировка (ORDER BY)
val sortOrder = "${ContactsContract.Contacts.DISPLAY_NAME} ASC"

val cursor = contentResolver.query(
    uri,
    projection,
    selection,
    selectionArgs,
    sortOrder
)

2. Cursor (Курсор)

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

Изначально курсор стоит перед первой строкой. Чтобы прочитать данные, нужно двигать его вперед.

cursor?.use { // use автоматически закроет курсор после работы
    while (it.moveToNext()) {
        // Получаем индексы колонок
        val nameIndex = it.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)
        val idIndex = it.getColumnIndex(ContactsContract.Contacts._ID)

        // Читаем данные
        val name = it.getString(nameIndex)
        val id = it.getLong(idIndex)

        println("Контакт: $name, ID: $id")
    }
}

Важно: Работа с базой данных и Content Provider — это тяжелая операция ввода-вывода. Никогда не вызывайте query() в главном (UI) потоке, иначе приложение зависнет (ANR). Используйте фоновые потоки (Coroutines, Threads).

3. Вставка (Insert), Обновление (Update), Удаление (Delete)

Эти операции работают аналогично, но используются реже, так как обычно мы читаем данные других приложений, а не меняем их.

  • insert() принимает URI и объект ContentValues (набор пар ключ-значение). Возвращает URI новой записи.
  • update() принимает URI, ContentValues и условие selection. Возвращает количество обновленных строк.
  • delete() принимает URI и условие selection. Возвращает количество удаленных строк.

Встроенные провайдеры Android

Android поставляется с набором мощных провайдеров, которые дают доступ к общим данным устройства. Вот самые популярные:

  1. Contacts Provider: Доступ к телефонной книге (имена, номера, email).
  2. MediaStore: Доступ к мультимедиа файлам (фото, видео, аудио). Это единственный правильный способ получить список всех фото на устройстве.
  3. Calendar Provider: Доступ к событиям календаря.
  4. Call Log Provider: История звонков.
  5. Settings Provider: Системные настройки (яркость, режим полета и т.д.).

Чтобы работать с ними, вам не нужно знать их точные строковые URI. В Android SDK есть контрактные классы (например, ContactsContract, MediaStore), которые содержат константы для всех URI и имен колонок.

Разрешения (Permissions)

Так как Content Provider часто открывает доступ к личным данным, система требует наличия специальных разрешений в AndroidManifest.xml.

Например, для чтения контактов:

<uses-permission android:name="android.permission.READ_CONTACTS" />

Начиная с Android 6.0 (API 23), «опасные» разрешения (как доступ к контактам или фото) нужно запрашивать у пользователя не только в манифесте, но и динамически во время работы приложения (показывая системное диалоговое окно).

Создание собственного Content Provider

Вам нужно создавать свой Content Provider только в двух случаях:

  1. Вы хотите поделиться данными своего приложения с другими приложениями.
  2. Вы хотите использовать встроенный механизм поиска (Search Framework) Android, который требует провайдера.

Если вы просто хотите использовать базу данных внутри своего приложения, не используйте Content Provider. Это избыточно и сложно. Используйте библиотеку Room или чистый SQLite.

Как объявить провайдер

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

<provider
    android:name=".MyContentProvider"
    android:authorities="com.example.myapp.provider"
    android:exported="true"
    android:readPermission="com.example.myapp.READ_DATA" />

Атрибут android:exported="true" делает провайдер доступным для других приложений. Если поставить false, он будет виден только вашему приложению.

Заключение курса по компонентам

В этой статье мы разобрали Content Provider — ключевой механизм для безопасного обмена данными в экосистеме Android.

Теперь вы знакомы со всеми четырьмя китами Android-разработки:

  1. Activity: То, что пользователь видит (UI).
  2. Service: То, что работает в фоне.
  3. Broadcast Receiver: То, что слышит события системы.
  4. Content Provider: То, что управляет данными.

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

Intent и Manifest: Связующие звенья архитектуры приложения

Intent и Manifest: Связующие звенья архитектуры приложения

Поздравляю! Вы уже знакомы с «Большой четверкой» компонентов Android: Activity, Service, Broadcast Receiver и Content Provider. Это кирпичи, из которых строится здание вашего приложения. Но если просто сложить кирпичи в кучу, дома не получится. Нужен цемент, который скрепит их вместе, и чертеж, по которому прораб (операционная система) будет вести строительство.

В этой статье мы разберем два механизма, которые превращают набор разрозненных классов в единую рабочую систему: AndroidManifest (чертеж) и Intent (связующий раствор или курьер).

AndroidManifest.xml: Паспорт приложения

Каждый Android-проект имеет файл AndroidManifest.xml, расположенный в корневой папке исходников. Это не просто конфигурационный файл, это паспорт вашего приложения.

Система Android не умеет сканировать ваш код в поисках классов. Перед тем как запустить приложение, система читает Манифест, чтобы понять:

  1. Какие компоненты (Activity, Service и т.д.) существуют в приложении.
  2. Какие разрешения (Permissions) требуются приложению (интернет, камера, контакты).
  3. Какие аппаратные и программные функции необходимы (например, наличие GPS или определенной версии Android).

Если вы создадите класс Activity, но забудете упомянуть его в Манифесте, для системы этого экрана просто не будет существовать. Попытка его запустить приведет к ошибке ActivityNotFoundException.

Структура Манифеста

Манифест — это XML-файл со строгой иерархией. Рассмотрим его ключевые элементы.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">

    <!-- Запрос разрешений -->
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.MyApp">

        <!-- Объявление Activity -->
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!-- Объявление второй Activity -->
        <activity android:name=".DetailActivity" />

    </application>

</manifest>

Разберем важные атрибуты:

  • package: Уникальный идентификатор вашего приложения (например, com.example.myapp). Именно по нему Google Play отличает одно приложение от другого.
  • uses-permission: Здесь мы сообщаем пользователю и системе, что нам нужен доступ к Интернету. Без этой строчки сетевые запросы работать не будут.
  • application: Корневой элемент, содержащий настройки всего приложения (иконка, название, тема).
  • activity: Каждый экран должен быть объявлен здесь. Атрибут android:name указывает на имя класса.

Обратите внимание на блок intent-filter внутри MainActivity. Строки ACTION_MAIN и CATEGORY_LAUNCHER говорят системе: «Этот экран является точкой входа. Помести иконку для его запуска в меню приложений».

Intent: Универсальный курьер

Теперь, когда система знает о наших компонентах благодаря Манифесту, нам нужно научиться переключаться между ними. Здесь на сцену выходит Intent (Намерение).

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

Представьте, что Intent — это конверт, который вы передаете операционной системе Android. На конверте написано, что нужно сделать, и, возможно, вложены какие-то документы (данные).

Intent используется для трех основных целей:

  1. Запуск Activity (переход на новый экран).
  2. Запуск Service (старт фоновой работы).
  3. Отправка широковещательного сообщения (Broadcast).

Существует два фундаментально разных типа интентов: Явные и Неявные.

1. Явные интенты (Explicit Intents)

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

Вы говорите системе: «Запусти мне DetailActivity».

// Мы находимся в MainActivity
val intent = Intent(this, DetailActivity::class.java)
startActivity(intent)

Здесь мы явно указываем класс DetailActivity. Это самый распространенный способ навигации между экранами одного приложения.

2. Неявные интенты (Implicit Intents)

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

Примеры:

  • «Я хочу открыть эту веб-ссылку» (подойдет любой браузер).
  • «Я хочу отправить этот текст другу» (подойдет любой мессенджер или почтовый клиент).
  • «Я хочу сделать фото» (подойдет любое приложение камеры).
// Хотим открыть сайт
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("https://www.google.com")

// Проверяем, есть ли приложение, способное обработать этот запрос
if (intent.resolveActivity(packageManager) != null) {
    startActivity(intent)
}

В этом случае система Android смотрит на Манифесты всех установленных приложений. Она ищет тех, у кого есть подходящий Intent Filter (фильтр намерений).

  • Если нашлось одно приложение — оно запускается сразу.
  • Если нашлось несколько (например, Chrome и Firefox) — пользователю показывается диалоговое окно выбора («Открыть с помощью...»).
  • Если не нашлось ни одного — приложение упадет с ошибкой, если не сделать проверку resolveActivity.

Передача данных через Intent

Intent — это не просто сигнал к запуску. Это еще и грузовик, перевозящий данные. Вы можете положить в Intent дополнительные параметры, используя метод putExtra().

Эти данные хранятся в структуре Bundle (связка) внутри интента. Это похоже на словарь (Map), где данные хранятся в виде пар «Ключ — Значение».

Отправка данных (Activity A):

val intent = Intent(this, UserProfileActivity::class.java)
intent.putExtra("USER_ID", 42)
intent.putExtra("USER_NAME", "Алексей")
startActivity(intent)

Получение данных (Activity B):

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_user_profile)

    // Достаем данные из интента, который запустил эту Activity
    val userId = intent.getIntExtra("USER_ID", -1) // -1 это значение по умолчанию
    val userName = intent.getStringExtra("USER_NAME")
}

Важные правила передачи данных

  1. Размер имеет значение: Не пытайтесь передать через Intent огромные объемы данных (например, картинку в высоком разрешении или большой JSON). Лимит буфера транзакции Binder составляет около 1 МБ для всех активных процессов. Если вы превысите его, приложение упадет с ошибкой TransactionTooLargeException. Для больших данных передавайте только ссылки (URI) или ID, а сами данные берите из базы данных или файла.
  2. Типизация: Всегда используйте правильные методы (getIntExtra, getStringExtra, getBooleanExtra) и следите за ключами. Хорошей практикой является создание констант для ключей, чтобы не ошибиться в написании строки.

Intent Filter: Как заявить о своих возможностях

Мы говорили, что неявные интенты ищут подходящие приложения. Но как приложение сообщает системе, что оно умеет делать? С помощью Intent Filter в Манифесте.

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

<activity android:name=".MyBrowserActivity">
    <intent-filter>
        <!-- Мы умеем ПРОСМАТРИВАТЬ данные -->
        <action android:name="android.intent.action.VIEW" />

        <!-- Мы поддерживаем категорию DEFAULT (обычный запуск) и BROWSABLE (запуск из браузера) -->
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />

        <!-- Мы умеем обрабатывать ссылки, начинающиеся с http и https -->
        <data android:scheme="http" />
        <data android:scheme="https" />
    </intent-filter>
</activity>

Теперь, если какое-то другое приложение отправит Intent с ACTION_VIEW и данными https://example.com, ваше приложение будет в списке кандидатов на открытие.

PendingIntent: Отложенное намерение

Существует особый вид интента — PendingIntent (Ожидающее намерение).

Обычный Intent выполняется немедленно. PendingIntent — это токен, который вы передаете другому приложению (обычно системному сервису), разрешая ему выполнить определенный Intent от вашего имени в будущем.

Самый частый пример — Уведомления (Notifications).

Когда вы создаете уведомление, вы хотите, чтобы при нажатии на него открылась ваша Activity. Но само нажатие происходит в шторке уведомлений (это системный интерфейс), когда ваше приложение может быть вообще закрыто. Вы передаете системе PendingIntent, говоря: «Вот тебе конверт. Если пользователь нажмет на уведомление, вскрой этот конверт и запусти то, что там написано, как будто это сделал я».

Заключение

AndroidManifest и Intent — это каркас и нервная система вашего Android-приложения.

  • Manifest определяет, кто вы есть, какие у вас права и из чего вы состоите.
  • Intent позволяет вашим компонентам общаться друг с другом и с внешним миром, передавая команды и данные.

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

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