Skip to content

Android SDK

Michael Bely edited this page Apr 29, 2024 · 177 revisions

Important

ВНИМАНИЕ!
ЭТОТ РАЗДЕЛ БОЛЬШЕ НЕ ПОДДЕРЖИВАЕТСЯ!
РОАДМАП ПЕРЕЕХАЛ В NOTION

Optimize for Doze and App Standby

Components
Activity, Service, BroadcastReceiver, ContentProvider


Manifest

Android Manifest
Важный файл в корне проекта, который описывает информацию о приложении для инструментов сборки, операционной системы и Google Play

Manifest Variables

<meta-data
    android:name="apiKey"
    android:value="${apiKey}"/>
defaultConfig {
    manifestPlaceholders += mapOf("apiKey" to "key")
}

Intent

Intent
Абстрактное описание выполняемой операции. Применяются для запуска activity, service, broadcast receiver

Explicit intent
Явный интент. Содержит явную информацию о классе или компоненте: точно знаем, что запускаем, например, activity или service. Чаще всего используются для старта компонентов внутри приложения

startActivity(Intent(context, MainActivity::class.java))

Implicit intent
Неявный интент. Не содержит информацию о конкретном компоненте. Система использует косвенные атрибуты, такие как action, type и category для выбора стартуемого компонента. Механизм поиска компонента по атрибутам неявного интента называется Intent Resolution, использует intent filters. Чаще всего используются для старта компонентов других приложений

startActivity(Intent(Intent.ACTION_CALL, "tel:88005553535".toUri()))

PendingIntent
Описание намерения и целевого действия, которое нужно выполнить с ним. Предоставляя PendingIntent другому приложению, вы предоставляете ему право выполнять указанную вами операцию, как если бы это другое приложение было вами. При создании в PendingIntent записывается информация о желаемом интенте и о том, какой компонент будет запущен. Объекты PendingIntent переживают остановку процесса, поэтому система может использовать PendingIntent для старта приложения. Пример использования PendingIntent – создание уведомления. Метод NotificationCompat.Builder.setContentIntent() принимает PendingIntent, который выполняется, когда пользователь кликает на нотификацию. PendingIntent нужен, чтобы другие приложения (уведомления, виджеты, аккаунт менеджер) могли запускать activity в нашем приложении
• Отложенное выполнение. PendingIntent используется для описания интента с отложенным выполнением. Самый популярный пример – Notification. При создании нотификации мы указываем PendingIntent, который будет выполнен, когда пользователь жмакнет на уведомление.
• Взаимодействие между процессами. PendingIntent переживает остановку процесса и используется для взаимодействия между процессами. Так же, может использоваться для старта приложения. Пример с push-уведомлениями – пушка может прийти, когда приложение будет не запущено, и в зависимости от логики, можно стартануть приложение

Context

Context
Божественный объект. Интерфейс к глобальной информации о среде приложения. Обеспечивает доступ к ресурсам и классам, специфичным для приложения, а также вызовы для операций на уровне приложения (запуск activity, service)

Application context vs Activity context
Оба являются экземплярами Context, но application context привязан к жизненному циклу приложения, а activity context к жизненному циклу Activity. Имеют доступ к разной информации о среде приложения

Application

Application
Базовый класс для поддержания глобального состояния приложения. Создается перед любым другим классом при создании процесса приложения

Application.onCreate()
Вызывается при запуске приложения до создания activity, service или receiver (за исключением ContentProvider). Реализации должны быть максимально быстрыми (например, с использованием ленивой инициализации), поскольку время, затраченное на эту функцию, напрямую влияет на производительность

registerActivityLifecycleCallbacks()
Метод, позволяющий зарегистрировать коллбэк, сообщающий о вызове методов lifecycle всех activity в приложении

Activity

The Activity Lifecycle
Restrictions on starting activities from the background
Где хранятся данные onSaveInstanceState()?

Activity
Представляет UI и функциональность, видимыe пользователю
• сворачиваем приложение: onPause() - onStop, разворачиваем: onStart() - onResume()
• смахиваем через task manager: onPause() - onStop() - onDestroy()
• переходим в task manager (recent menu): lifecycle methods не вызываются
• переходим на другую activity внутри приложения методом startActivity(): onPause() - onStop(), возвращаемся обратно: onStart() - onResume()
• смена ориентации, переключение темы (activity пересоздается): onPause() - onStop() - onDestroy() - onCreate() - onStart() - onResume()
• если activity будет убита из-за low memory могут не вызваться killable-методы onStop() и onDestroy() (onPause() до версии 3.0)
• открываем DialogFragment: lifecycle methods не вызываются
• открываем permission dialog: onPause(), закрываем: - onResume()
• очистить бэкстек при создании activity: использовать флаг FLAG_ACTIVITY_CLEAR_TOP, использовать вместе FLAG_ACTIVITY_CLEAR_TASK и FLAG_ACTIVITY_NEW_TASK

Activity Lifecycle
onCreate() – вызывается 1 раз, когда Activity создается. Внутри него вызывается метод setContentView()
onStart() – activity отрисована и видна пользователю
onResume() – вызывается перед тем, как Activity станет доступна для взаимодействия с пользователем
onPause() – метод симметричный onResume(), пользователь больше не может взаимодействовать с Activity, но она остается видимой
onStop() – метод симметричный onStart(), вызывается, когда Activity больше не видна пользователю
onDestroy() – метод симметричный onCreate(), вызывается перед тем, как Activity будет уничтожена системой

Launch Modes
standart - Режим по умолчанию. Поведение Activity, установленного в этот режим, будет всегда создавать новую Activity, чтобы работать отдельно с каждым отправленным Intent. По сути, если для составления электронного письма отправлено 10 Intent-ов, должно быть запущено 10 Activity, чтобы обслуживать каждый Intent отдельно. В результате на устройстве может быть запущено неограниченное количество таких Activity
singleTop - Он ведет себя почти так же, как и standard, что означает, что экземпляров singleTop Activity можно создать столько, сколько мы захотим. Единственное отличие состоит в том, что если уже есть экземпляр Activity с таким же типом наверху стека в вызывающей задаче, не будет создано никакого нового Activity, вместо этого Intent будет отправлен существующему экземпляру Activity через метод onNewIntent()
singleTask - Activity с singleTask разрешено иметь только один экземпляр в системе (аля синглтон). Если в системе уже существует экземпляр Activity, вся задача, удерживающая экземпляр, будет перемещен наверх, а Intent будет предоставлен через метод onNewIntent(). В противном случае будет создано новое Activity и помещено в соответствующую задачу. Если в системе еще не было экземпляра singleTask Activity, будет создан новый, и он будет просто помещен вверх стека в той же задаче
singleInstance - Этот режим очень похож на singleTask, где в системе мог существовать только один экземпляр Activity. Разница в том, что задача, которая располагает этим Activity, может иметь только одно Activity — то, у которого атрибут singleInstance. Если из этого вида Activity вызывается другое Activity, автоматически создается новое задание для размещения этого нового Activity. Аналогичным образом, если вызывается singleInstance Activity, будет создана новая задача для размещения этого Activity

Как пережить поворот экрана?
При повороте экрана активити уничтожается и создается заново. Вызываются коллбэки onPause(), onStop(), onSaveInstanceState(), onDestroy()onCreate(), onStart(), onRestoreInstanceState(), onResume(). Чтобы сохранить состояние активити, вы должны переопределить метод onSaveInstanceState() и положить данные в Bundle. При реинициализации активити, Bundle с сохраненным состоянием передается в onCreate() и в onRestoreInstanceState(). Система вызывает onSaveInstanceState() и onRestoreInstanceState() только в том случае, когда необходимо сохранить состояние, например при повороте экрана или при убийстве активити для освобождения памяти. Данные коллбэки не вызываются, если пользователь выходит из активити нажав Back или если активити убивается вызовом finish(). onSaveInstanceState() вызывается после onStop() на версии API ≥ 28. На API < 28 этот коллбэк вызывается перед onStop() и нет гарантий до или после onPause(). onRestoreInstanceState() вызывается после onStart().

Когда onDestroy вызовется без onPause и onStop?
finish() в onCreate()

Как изменилось поведение onResume() и onPause() в Android 10?
В Android 10 добавлена поддержка foldables и девайсов с большим экраном. В связи с этим было изменено поведение коллбэков onResume() и onPause() в режиме multi-window. В Android 9 только активити, с которой взаимодействует пользователь, находилась в состоянии resumed, а все остальные активити на экране имели состояние paused. Начиная с Android 10, все видимые активити в режиме multi-window находятся в состоянии resumed. Это поведение называется multi-resume. Активити, с которой взаимодействует пользователь, называется topmost resumed. Для того, чтобы различать resumed и topmost resumed активити, в Android 10 добавлен коллбэк onTopResumedActivityChanged(isTopResumed: Boolean). Этот метод вызывается, когда активити получает или теряет состояние topmost

Как запустить стек из нескольких активити?
Для старта стека из нескольких активити используется класс TaskStackBuilder. После вызова метода startActivities() (см. картинку), стартует только activity3. Информация об activity1 и activity2 хранится в стеке. Когда пользователь нажимает «назад», или на activity3 вызывается метод finish(), создается и стартует activity2. Этот механизм полезен для реализации роутинга при запуске приложения через deep link

val taskStackBuilder = TaskStackBuilder.create(context)
    .addNextIntent(Intent(context, Activity1::class.java))
    .addNextIntent(Intent(context, Activity2::class.java))
    .addNextIntent(Intent(context, Activity3::class.java))
taskStackBuilder.startActivities()

OnBackPressedDispatcher

class MyFragment: Fragment() {
  
    override fun onAttach(context: Context) {
        super.onAttach(context)

        val callback = object: OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                // handle action
            }
        }
        requireActivity().onBackPressedDispatcher.addCallback(this, callback)
    }
}

taskAffinity

Service

Погружение в службы Android

Service
Компонент для выполнения длительных операций в фоновом режиме. Пример: проигрывание музыки, загрузка файла
• по умолчанию работают в главном потоке (UI thread, не main thread, так как он хоть и главный, но не всегда является потоком пользовательского интерфейса)
• сервисы нужны, чтобы связать их lifecycle с lifecycle приложения, так как обычные потоки не всегда совпадут с activity lifecycle (+возможность запуститься не только из activity)
• как работает: в момент создания сервиса вызывается onCreate(), затем запускается thread или executor, который после завершения дает знать сервису, чтобы тот вызвал stopSelf(). Наша задача: сообщить сервису о начале и завершении работы
• чтобы сервис работал после закрытия activity нужно в течение 5 секунд после его запуска отобразить уведомление, иначе ANR
• stopSelf() - завершить работу сервиса
• Intent.stopService() - завершить работу сервиса через intent, вызовется stopSelf() - onDestroy()

Foreground
Сервис, о котором пользователь уведомлен. Выполняется в UI thread. Во время выполнения отображает уведомление. Пример: аудиоплеер. Приоритет больше, чем background
• stopForeground(removeNotification: Boolean) - завершит выполнение сервиса на переднем плане и опционально удалит уведомление

Background
Выполняется в фоновом потоке. Начиная с API 26, приложения в фоне не могут создавать фоновые сервисы, решением в этом случае может быть WorkManager. В Android 8.0 (API level 26) введены ограничения на работу с фоновыми сервисами. Эти ограничения касаются приложений с targetSdkVersion ≥ 26, но пользователь может включить ограничения для всех приложений в настройках. Начиная с Android 8.0, Фоновые сервисы работают пока пользователь взаимодействует с приложением. Система убивает все фоновые сервисы через несколько минут после того, как пользователь покидает приложение. Нельзя запустить фоновый сервис для приложения, с которым не взаимодействует пользователь. Вместо фоновых сервисов рекомендуется использовать JobScheduler или видимые сервисы. Чтобы запустить видимый сервис, нужно сначала запустить фоновый сервис методом startService(), а потом вызвать метод startForeground() на этом сервисе. Но, начиная с Android 8.0, нельзя запустить фоновый сервис, если пользователь не взаимодействует с приложением. Поэтому в API level 26 был добавлен метод startForegroundService(), который делает то же, что и startService(), но обещает системе, что метод startForeground() будет вызван сразу после старта сервиса. Если не вызывать startForeground(), то система убьет сервис через несколько секунд

Bound
Клиент-серверный подход: посылаем запросы, получаем результаты

IBinder
Используется для синхронного взаимодействия с сервисом (переключение треков в плеере). С обычным сервисом можно взаимодействовать только через методы Intent startService() и stopService(), которые обработаются в onStartCommand(). Через IBinder можно вызывать методы сервиса напрямую

IntentService
Запускается и работает в фоновом потоке из коробки, в отличие от обычных сервисов, работающих в главном потоке. Выполняют работу в методе onHandleIntent(), после чего останавливается. Операции в onHandleIntent() не могут быть прерваны, связь с основным потоком отсутствует, можно вернуть результат через уведомление или broadcast receiver. На IntentService распространяются все ограничения на работу фоновых сервисов, введенные в API 26. Начиная с API 30 - является устаревшим, рекомендуется заменить на WorkManager или JobIntentService

JobIntentService
Используется для тех же целей, что и IntentService, и имеет похожий API. Для старта используется статический метод enqueueWork(), который использует Context.startService() для API level < 26 и JobScheduler.enqueue() для API level ≥ 26. После этого система стартует сервис и вызывает в фоновом потоке метод onHandleWork()

JobScheduler
API для планирования различных типов заданий в рамках платформы, которые будут выполняться в собственном процессе приложения. В версии для Android 5.0 максимальное время выполнения заданий составляло одну минуту. Начиная с версии Android 6.0 и заканчивая версией Android 11, максимальное время выполнения заданий составляло 10 минут. Начиная с версии Android 12, задания по-прежнему будут останавливаться через 10 минут, если система занята или нуждается в ресурсах, но в противном случае задания могут продолжать выполняться дольше 10 минут

Lifecycle
Различается для started и bound сервисов
• onCreate() – вызывается один раз, когда сервис создается системой. Для создания started сервиса используется метод startService() или bindService()
• onStartCommand(intent, flags, startId): Int – вызывается, когда сервис переходит в активное состояние. Код, который выполняет сервис, должен быть написан в этом методе. Вызовется столько раз, сколько был запущен сервис
• onBind() – вызывается системой, когда первый клиент присоединяется к сервису вызовом метода bindService(). После вызова этого метода bound сервис переходит в активное состояние.
• onUnbind() – вызывается системой, когда все клиенты отсоединились от сервиса вызовом метода unbindService().
• onDestroy() – вызывается, когда сервис уничтожается системой. Это происходит после вызова stopSelf() или stopService(). Также система может убить процесс с фоновым сервисом когда не хватает ресурсов или, начиная с Android 8.0, для ограничения фоновых работы или вызывается после onUnbind(), перед тем как система уничтожит сервис

onStartCommand(): Int
После того, как мы вызываем startService() срабатывает onStartCommand(), который возвращает одну из констант
• START_NOT_STICKY – сервис не будет перезапущен после того, как был убит системой
• START_STICKY – сервис будет перезапущен после того, как был убит системой
• START_REDELIVER_INTENT – сервис будет перезапущен после того, как был убит системой. Кроме этого, сервис снова получит все вызовы startService, которые не были завершены методом stopSelf()

Foreground service type
В Android 10 добавили атрибут foregroundServiceType для элемента в AndroidManifest. Этот атрибут дает понять системе к какой категории отнести сервис. В API level 29 существует 6 типов сервисов. Тип location влияет на список необходимых пермишенов для запроса локации. Получение локации из сервиса с типом location считается запросом из фореграунда и пермишн ACCESS_BACKGROUND_LOCATION не требуется. Остальные типы пока не имеют прямого влияния на работу приложения, но это может измениться в будущих версиях API
• connectedDevice - следит за носимым фитнес-трекером
• dataSync - загрузка файлов из сети
• location - слежка за локацией пользователя
• mediaPlayback - воспроизведение аудиокниги, подкаста или музыки
• mediaProjection - запись видео с дисплея устройства
• phoneCall - обработка телефонного звонка

AlarmManager
Доступ к службам системной сигнализации. Они позволяют запланировать запуск вашего приложения в какой-то момент в будущем. При срабатывании будильника Intentсистема транслирует зарегистрированное для него сообщение, автоматически запуская целевое приложение, если оно еще не запущено. Зарегистрированные будильники сохраняются, пока устройство находится в спящем режиме (и могут дополнительно разбудить устройство, если они сработают в это время), но будут удалены, если оно будет выключено и перезагружено. AlarmManager предназначен для случаев, когда вы хотите, чтобы код вашего приложения выполнялся в определенное время, даже если ваше приложение в данный момент не запущено. Для обычных операций синхронизации (тиков, тайм-аутов и т.д.) проще и эффективнее использовать Handler

WakeLock
Чтобы не разрядить аккумулятор, Android-устройство, которое не используется, быстро засыпает. Однако бывают случаи, когда приложению необходимо разбудить экран или ЦП и поддерживать его в активном состоянии, чтобы выполнить некоторую работу

BroadcastReceiver

Broadcasts overview

BroadcastReceiver
Используется для отправки и получения различных событий, например: загрузка системы, смена языка/способа ввода, переход в режим полета, зарядка батареи. Приложения могут уведомлять друг друга о загрузке новых данных. Приложения могут получать события 2 способами: через ресиверы, объявленные в манифесте и ресиверы, зарегистрированные в контексте. Если объявлен в манифесте, система запустит приложение при отправке рассылки. Диспетчер системных пакетов регистрирует приемник при установке приложения. Затем приемник становится отдельной точкой входа в приложение, что означает, что система может запустить приложение и доставить трансляцию, если он в данный момент не запущено. Система создает новый BroadcastReceiver объект для обработки каждой получаемой рассылки. Этот объект действителен только на время вызова onReceive(Context, Intent) (вызывается в основном потоке). Как только код возвращается из этого метода, система считает, что компонент больше не активен. Можно установить ограничения как для отправителя, так и для получателя рассылки. Разрешения позволяют ограничивать трансляции набором приложений, обладающих определенными разрешениями. В Android 4.0 и выше можно указать пакет с setPackage(String) при отправке трансляции. Система ограничивает трансляцию набором приложений, соответствующих пакету

sendOrderedBroadcast(Intent, String)
Отправляет сообщения одному получателю за раз. Поскольку каждый получатель выполняется по очереди, он может передать результат следующему получателю или полностью прервать широковещательную рассылку, чтобы она не передавалась другим получателям. Запущенными получателями заказов можно управлять с помощью атрибута android:priority соответствующего фильтра намерений; приемники с одинаковым приоритетом будут запускаться в произвольном порядке

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

LocalBroadcastManager.sendBroadcast
Отправляет широковещательные сообщения получателям, которые находятся в том же приложении, что и отправитель. Если вам не нужно отправлять трансляции между приложениями, используйте локальные трансляции. Реализация намного эффективнее (межпроцессное взаимодействие не требуется), и вам не нужно беспокоиться о каких-либо проблемах безопасности, связанных с тем, что другие приложения могут получать или отправлять ваши трансляции

Ограничения
• Начиная с Android 7.0 (API 24) не работают ACTION_NEW_PICTURE и ACTION_NEW_VIDEO, и объявление получателя в манифесте для CONNECTIVITY_ACTION
• Начиная с Android 8.0 (API 26) система ограничивает получателей, объявленных в манифесте для большинства трансляций не нацеленных конкретно на приложение, можно регистрировать вручную
• Начиная с Android 9 (API 28) не работает получение данных о местонахождении пользователя или данные, позволяющие установить личность (NETWORK_STATE_CHANGED_ACTION)

ContentProvider

Content provider basics
Calendar provider
Contacts Provider

ContentProvider
Предоставляет контент для приложений через единый интерфейс. Пример: доступ к словарям пользователя, хранилищу, контактам, календарям. ContentProvider работает как ворота: может дать доступ к файлу, или не дать, позволяет удалять файлы, проверяет permissions, которые рекомендуется использовать меньше, если это не файловый менеджер, для файлов есть специальный FileProvider, который может временно предоставлять доступ к файлу по uri, эта модель безопасна, когда в onActivityResult возвращается intent там есть флаг GRANT_URI_PERMISSION

Permissions

RequiresPermission
Request location permissions

Runtime Permissions (Dangerous)
Опасные разрешения, предоставляющие доступ к данным с ограниченным доступом и действиям, которые существенно влияют на систему и другие приложения. Начиная с API 23 перед перед выполнением опасных действий нужно запросить у пользователя разрешение в runtime
• dangerous samples: location, contacts, storage, contacts, phone calls, camera, microphone

Fragment

Fragment
Часть UI, внутри Activity с lifecycle
• нельзя создать через конструктор, так как иначе система не восстановит состояние фрагмента, когда это будет нужно
• получить ссылку на фрагмент из activity: findFragmentByTag() и findFragmentById()

Чем отличается tag в методах add() и addToBackStack()?
Tag в методе add() присваивается фрагменту. Fragment.getTag() возвращает этот тег. Тег фрагмента используется в методе findFragmentByTag(). Tag в методе addToBackStack() это на самом деле не tag, а name. Имя транзакции, которое присваивается объекту BackStackEntry и возвращается методом getName().

setRetainInstance()
Метод setRetainInstance() принимает boolean параметр. По умолчанию значение retainInstance фрагмента – false. Если retainInstance выставлен в true, то фрагмент переживает пересоздание хост-активити, например при повороте экрана. Когда активити пересоздается, фрагмент с retainInstance=true отсоединяется от старой активити и присоединяется к новой. Поэтому при пересоздании активити у фрагмента не вызываются методы onDestroy() и onCreate(), но вызываются onDetach(), onAttach() и onActivityCreated(). setRetainInstance() может быть использован только на фрагментах, не добавленных в backstack.

Activity State Loss Exception? Для чего нужен commitAllowingStateLoss()?
Это исключение вида: IllegalStateException: Can not perform this action after onSaveInstanceState with MyFragment. Генерируется, когда метод FragmentTransaction.commit() вызывается после Activity.onSaveInstanceState(). Когда пользователь уходит с активити, состояние сохраняется на случай, если активити будет уничтожена системой. Сохранение состояния происходит в методе onSaveInstanceState(). Если транзакция применяется после сохранения состояния, то транзакция не может быть сохранена и система бросает исключение. Метод commitAllowingStateLoss() делает то же, что и метод commit(), но говорит системе, что мы готовы к потере состояния и исключение бросать не нужно. Использование commitAllowingStateLoss() приводит к ситуациям такого вида: Пользователь возвращается на активити, которая была уничтожена системой. Пользователь ожидает увидеть UI, который отображался перед тем как он покинул активити, но транзакция с добавлением или удалением фрагмента не сохранилась и пользователь видит пустой экран или активити в неверном состоянии. Хорошей практикой считается использование commit(), а не commitAllowingStateLoss(). Если вы получаете репорты о state loss крэшах, пробуйте решить корень проблемы. Для этого не вызывайте commit() в onPause() или следующих после него методах. Тема Activity State Loss хорошо раскрывается в этом посте.

Lifecycle
onAttach() - Когда фрагмент добавляется в FragmentManager и прикрепляется к Activity
onCreate() - Когда фрагмент создается. Этот метод вызывается после вызова соответствующего метода onCreate() у Activity
onCreateView() - Создается иерархия view. В этом методе можно установить, какой именно UI будет использовать фрагмент
onViewCreated() - Вызывается после создания представления фрагмента
onAcyivityCreated() - Вызывается после того, как отрабатывает метод Activity.onCreate()
onViewStateRestored() - Вызывается, когда состояние иерархии View восстановлено
onStart() - Вызывается, когда фрагмент становится видимым пользователю
onResume() - Вызывается, когда фрагмент становится активным для взаимодействия с пользователем
onPause() - Фрагмент остается видимым, но с ним уже нельзя взаимодействовать
onStop() - Фрагмент становится не видим пользователю
onDestroyView() - Метод, в котором фрагмент очищает ресурсы, связанные с иерархией View
onDestroy() - Вызывается перед тем, как фрагмент будет уничтожен системой
onDetach() - Вызывается перед тем, как фрагмент будет отсоединен от активити

FragmentManager
Класс, отвечающий за работу с фрагментами: добавление, удаление, замена, backstack. Чтобы фрагмент не потерял состояние, можно проверять в каком состоянии находится fragmentManager, прежде, чем коммитить туда
add() - добавить фрагмент поверх предыдущего, методы onPause(), onResume() и onCreateView() у предыдущего вызываться не будут, они оба будут активны
remove() - удалить фрагмент
replace() - удалит текущий фрагмент и добавит новый, если есть back stack, то у предыдущего вызовется onCreateView(), удалит все фрагменты, добавленные через add()
addToBackStack() - добавляет транзакцию в back stack
popBackStack() - асинхронная функция, удаляет транзакцию с верхушки стека, вернет true, если back stack хранил хотя бы одну транзакцию
popBackStackImmediate() - похож на popBackStack, но выполняет операции немедленно внутри вызова, синхронно как executePendingTransactions
commit() - асинхронный метод, транзакция добавляется в очередь главного потока и выполняется при первой возможности. Если вызвать executePendingTransactions() после метода commit(), то транзакция станет синхронной
commitNow() - синхронный метод
commitAllowingStateLoss() - работает как commit() + мы говорим системе, что готовы к потере состояния и исключение бросать не нужно
executePendingTransactions() - вызывается из основного потока, чтобы немедленно выполнить транзакцию

Sensor

Sensor
Предоставляет методы для определения возможностей, которые доступны конкретному датчику

SensorManager
Предоставляет методы регистрации активности с датчиков и их калибровки

SensorEvent
Предоставляет необработанные данные датчика, включая информацию о точности

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

SharedPreferences

SharedPreferences
Интерфейс для доступа к настройкам и их изменения

commit()
Синхронно записывает настройки в хранилище. Когда 2 редактора одновременно изменяют настройки, побеждает тот, который последним вызвал фиксацию

apply()
Асинхронно записывает изменения на диск и не уведомляет об ошибках. Если другой редактор в этом SharedPreference выполняет commit(), в то время как apply() еще не завершен - commit() будет блокироваться до тех пор, пока все асинхронные фиксации не будут завершены

Notifications

Notification Channel
Начиная с Android 8.0 (API level 26) для каждого уведомления необходимо указывать канал. Пользователь может управлять визуальным и звуковым поведением, которое будет применяться ко всем уведомлениям в канале. Например, разные каналы в приложении часы для будильников, таймеров, секундомеров

Bundle

Bundle
Класс, реализующий ассоциативный массив, т.е. хранящий пары ключ-значение. Имеет get() и put() методы для примитивов, строк и объектов, которые реализуют интерфейсы Parcelable и Serializable. Используется для передачи данных между базовыми компонентами и для сохранения состояния действий
• указывать capacity для лучшего перфоманса
• не реализует equals() и hashCode()
• буфер транзакций Binder имеет фиксированный размер 1 MB, который используется всеми транзакциями в процессе, если размер превышен - TransactionTooLargeException

val bundle: Bundle = Bundle(10)

val bundle: Bundle = bundleOf(
    "name" to "John",
    "age" to 80
)

Drawable

Drawable
Общая абстракция для чего-то, что можно нарисовать. Чаще всего - тип ресурса. В отличие от View не имеет возможностей получать события и взаимодецствовать с пользователем

Testing

Full Guide to Testing Android Applications in 2022

UnitTest
Тестирование бизнес-логики
JUnit - фреймворк тестирования для JVM
Robolectric - среда модульного тестирования для Android
Mockito - фреймворк для модульных тестов

AndroidTest
Тестирование пользовательского интерфейса. Если UI не стабилен и часто меняется, тесты начинают блокировать пайплайн и приходится тратить время на их апдейт, а не на увеличение тестового покрытия
Espresso - Фреймворк для UI тестов
Kaspresso - основан на Espresso, читаемый DSL

Window

Window
Это абстрактный класс, который не является наследником Activity, Fragment или View. Класс Window контролирует что и как рисуется на экране. Активити имеет один инстанс Window, который можно получить методом getWindow(). Window, в свою очередь, имеет объект Surface и единственную иерархию View. Android-приложение использует WindowManager для создания объектов типа Window и Surface, на котором рисуется контент Window. Когда UI должен обновиться, на объекте Surface вызывается метод lockCanvas(), который возвращает объект типа Canvas. Canvas передается вниз по иерархии View, ассоциированной с Window, и каждая view рисует себя на канвасе.

AppWidget

Ограничения
Виджеты ограничены по жестам, которые они могут обрабатывать. Доступны только touch и вертикальный swipe. Виджеты поддерживают только следующие лэйауты: FrameLayout, LinearLayout, RelativeLayout, GridLayout. Набор view тоже ограничен (полный список в документации). Причина этого в том, что лэйауты виджетов основаны на RemoteViews.

RemoteViewsService
RemoteViewsService используется для создания виджетов (App Widget), которые отображают коллекции элементов. Доступ к данным, которые виджет показывает в виде коллекции, предоставляется через ContentProvider. В качестве адаптера выступает класс, реализующий интерфейс RemoteViewsFactory. Этот класс является прослойкой между данными в ContentProvider и UI-коллекцией. В качестве элементов коллекции выступают объекты RemoteViews. RemoteViewsService – это сервис, который который связывает виджет с определенной реализацией RemoteViewsFactory. Для создания виджета с коллекцией необходимо реализовать интерфейс RemoteViewsFactory и абстрактный класс RemoteViewsService.

Lifecycle
onEnabled() - вызывается, когда пользователь добавляет на home screen первый инстанс виджета. При добавлении новых инстансов того же типа, onEnabled() вызываться не будет. В этом методе следует выполнять общие для всех инстансов виджета операции инициализации
onUpdate() - вызывается при добавлении инстанса виджета. Также вызывается периодически через интервал времени, который был задан в AppWidgetProviderInfo. Этот метод используется для инициализации и обновления состояния
onAppWidgetOptionsChanged() - вызывается при добавлении и при изменении размера
onDeleted() - вызывается при удалении инстанса виджета
onDisabled() - вызывается при удалении всех инстансов виджета. В этом методе следует освобождать ресурсы, созданные при инициализации в методе onEnabled()

Collections

ArrayMap
Коллекция, предназначенная для более эффективного использования памяти, чем традиционная HashMap

val map: ArrayMap<Int, String> = arrayMapOf(1 to "Tom", 2 to "Sam", 3 to "Bob")

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

val set: ArraySet<String> = arraySetOf("Tom", "Sam", "Bob")

Concurrent

Thread Annotations
Как работает UI в Android. Не все так сложно
Main Loop (Главный цикл) в Android Часть 1. Пишем свой цикл

Thread safe (потокобезопасный) - может быть безопасно вызван из любого потока

Main Thread
Поток пользовательского интерфейса

Worker Thread
Просто еще один поток для фоновых операций, чтобы не прерывать выполнение пользовательского интерфейса

Looper
Бесконечный цикл, который получает из очереди MessageQueue сообщения и выполняет их. Чтобы создать Looper, нужно вызвать метод Looper.prepare(). После этого метод Looper.prepare() сохраняет созданный объект Looper в ThreadLocal. Looper у потока может быть только один

ThreadLocal
Это просто ConcurrentHashMap<Thread, T>, коллекция, в которой ключом является объект потока, а значением любой объект который нам нужен. Этот Map позволяет внутри потока достать объект предназначенный для него в любой момент времени. Из любого потока можно через Looper.myLooper() получить лупер, связанный с текущим потоком. При условии, что Looper есть в этом потоке

MessageQueue
Специальная очередь из объектов Message, простой односвязный список. Модель построенная на очереди и сообщениях позволяет откладывать выполнение задачи, отменять не нужные задачи и самое важное: эта модель позволяет делать GUI в одном потоке. Один поток позволяет не париться о том, меняет ли эту View какой-то другой поток или нет

Message
Сообщение, содержащее описание и произвольный объект данных, которое можно отправить в Handler. Message можно создать через обычный конструктор, однако так лучше не делать. Для создания лучше использовать метод Message.obtain(). Message.obtain() возвращает объект Message из пула. Сделано это для оптимизации, чтобы не тратить память каждый раз когда там нужен Message. В этом пуле может быть максимум 50 сообщений. Если все сообщения пула используются, то Message.obtain() создает и возвращает новый объект Message. Методы:
callback - runnable, который выполнит Looper
next - ссылка на следующий Message
time - отметка времени, когда сообщение должно быть выполнено

Handler
Отправляет сообщения (Message) через MessageQueue в Looper. Handler предоставляет удобный API для отправки Message в Looper. В примере ниже мы создаем Runnable с вызовом метод doSmth(), далее Handler обернет этот Runnable в Message и отправит в Looper который его выполнит. Когда вызываем метод post, переданный Runnable оборачивается в Message и отправляется на Looper потока. Асинхронность UI обусловлена вызовом методов жизненного цикла через Looper. Каждый раз когда мы создаем транзакцию для показа фрагмента, после вызова метода commit эта транзакция отправляется через Handler в Looper главного потока. Эта транзакция выполняется позже. Это же происходит и с показом новой Activity, анимациями и многими другими вещами

val looper: Looper = requireNotNull(Looper.myLooper())
val handler: Handler = Handler(looper)
handler.post { 
    doSmth() 
}

runOnUiThread
Метод Activity. Изменить View из фонового потока. Под капотом отправляет Runnable через Handler в Looper главного потока

Parcelable

Parcelable
Интерфейс, который совместно с классом Parcel реализует механизм сериализации в Android. Он более оптимизирован под Android, чем Serializable и используется, чтобы гонять данные в межпроцессорном взаимодействии

Если класс реализует интерфейс Parcelable, поля класса сериализуются в методе writeToParcel(). Также parcelable класс обязан иметь статическое ненулевое поле, названное CREATOR типа Creator. Интерфейс Creator имеет два метода createFromParcel(parcel: Parcel): T и newArray(size: Int): Array. Эти методы обратные writeToParcel() и используются для чтения данных из Parcel и создания объекта. Объект Parcelable записывается в контейнер Parcel, который имеет метод marshall(): Array для представления объекта в виде массива байтов. Parcel предназначен для передачи данных при межпроцессорной коммуникации. При изменении структуры объекта или реализации метода writeToParcel() байтовое представление, которое возвращается методом marshall(), будет изменено. Поэтому строго не рекомендуется записывать его в персистентное хранилище.

Что быстрее parcelable или serializable?
В режиме «ручного управления» быстрее Serializable, тогда он не использует reflection и сериализует поля вручную через методы writeObject() и readObject(). В другие случаях быстрее Parcelable, потому что разработчик управляет методами преобразования

LruCache

LruCache
Кэш, который содержит ссылки на ограниченное количество значений. При каждом доступе к объекту оно перемещается в начало очереди. При добавлении объекта, если кэш переполнен, значение в конце вытесняется и может быть собрано сборщиком мусора. По умолчанию размер кэша измеряется количеством записей. Следует использовать, когда загрузка ресурсов влияет на отзывчивость приложения (хранить эскизы изображений)

val maxMemory: Long = Runtime.getRuntime().maxMemory() / 1024
val cacheSize: Int = (maxMemory / 8).toInt()

val lruCache: BitmapCache = BitmapCache(cacheSize)

class BitmapCache(maxSize: Int): LruCache<String, Bitmap>(maxSize) {

    fun getBitmapFromMemory(key: String): Bitmap? {
        return this.get(key)
    }

    fun setBitmapToMemory(key: String, bitmap: Bitmap) {
        if (getBitmapFromMemory(key) == null) {
            this.put(key, bitmap)
        }
    }
}
Clone this wiki locally