Что такое activity и зачем оно нужно
Activity (Активность, Деятельность)
Что такое Activity
Разработчики со стажем могут воспринимать Активность как форму. Простые приложения состоят из одной активности. Более сложные приложения могут иметь несколько окон, т.е. они состоят из нескольких активностей, которыми надо уметь управлять и которые могут взаимодействовать между собой.
Активность, которая запускается первой, считается главной. Из нее можно запустить другую активность. Причем не только ту, которая относится к нашему приложению, но и другого приложения. Пользователю будет казаться, что все запускаемые им активности являются частями одного приложения, хотя на самом деле они могут быть определены в разных приложениях и работают в разных процессах. Попробуйте воспринимать активности как страницы разных сайтов, открываемых в браузерах по ссылке.
Обычно активность занимает весь экран устройства, но это не является обязательным требованием. Вы можете создавать полупрозрачные и плавающие окна активностей. И с развитием Android такой подход набирает обороты.
Чтобы создать активность, нужно унаследоваться от класса Activity и вызвать метод onCreate(). В результате мы получим пустой экран. Толку от такого экрана никакого. Поэтому в активность добавляют компоненты, фрагменты с помощью разметки.
Жизненный цикл активности
Активность имеет жизненный цикл — начало, когда Android создает экземпляр активности, промежуточное состояние, и конец, когда экземпляр уничтожается системой и освобождает ресурсы. Активность может находиться в трех состояниях:
Если активность, которая была уничтожена системой, нужно снова показать на экране, она должна быть полностью перезапущена и восстановлена в своем предыдущем состоянии.
Активность в виде диалогового окна
Помните, изучая темы, мы создали вторую активность в виде диалогового окна. С таким же успехом вы можете создать окно и для основной активности. Добавим в манифесте для активности строчку:
Напишем код для запуска:
Обратите внимание, что в данном примере мы не используем разметку, а программно создаём экземпляр класса TextView с необходимыми свойствами (текст, отступы) и передаём его в метод setContentView().
Програмнное создание активности
В основном мы пользуемся XML-разметкой для формирования внешнего вида активности. Это рекомендованный способ и в 99% вы будете использовать именно его. Но в некоторых случаях возможно вам понадобится создать активность программно. Сложного там ничего нет, для общего развития пригодится.
Если в стандартном случае мы подключаем XML-файл в методе setContentView(R.layout.activity_main), то при программном создании данный метод нам не понадобится. Удаляем его и пишем код:
В данном примере мы также присваиваем компонентам идентификаторы через метод setId(), хотя необходимости в этом не было. В примере эта возможность показана для демонстрации. Если вы используете идентификаторы, то в файле res/values/stings.xml добавьте строчки:
Обычно опытные программисты создают отдельный файл для идентификаторов, например, res/values/ids.xml.
Классы Activity в Android
Android SDK включает набор классов, наследованных от Activity. Они предназначены для упрощения работы с виджетами, которые часто встречаются в обычном пользовательском интерфейсе. Перечислим некоторые из них (наиболее полезные).
Жизненный цикл Activity
Основным компонентом Android-приложения является Activity, определяющая интерфейс окна. Пользователи выполняют определенные действия в приложении через Activity : делают фото/видео, отправляют письмо, отвечают на звонок и т.д. Для каждой активности создается окно с соответствующим пользовательским интерфейсом. Как правило, окно занимает весь экран, но может быть и меньше.
Но прежде чем говорить об активности, необходимо пару слов сказать об операциях. Система Android работает с операциями. Каждой операции соответствует определенное окно (активность) для представления пользовательского интерфейса.
Операции
Android приложение может включать несколько слабо связанных друг с другом операций, одна из которых является «главной» и выполняется при первом старте приложения. Любая из операций может стартовать другую для выполнения определенных действий. Каждый раз, когда запускается какая-либо операция, предыдущая останавливается, и система сохраняет новую операцию в стеке типа FIFO («first-in-first-out»), т.е. «последним пришел — первым вышел». После завершения пользователем определенных действий или нажатии кнопки «Назад», текущая операция удаляется из стека и уничтожается, а предыдущая операция возобновляет своё функционирование.
Операция может даже стартовать операции в других приложениях на устройстве. Например, работу с почтой могут выполнять несколько приложений. Одно из приложений подготавливает информацию для отправки и определяет адресата/ов. При выполнении команды отправки стартует операция другого приложения. При этом действие отправляемой операции приостанавливается. После отправки сообщения первое приложение возобновляет работу операции, и все выглядит так, будто операция отправки электронной почты является частью первого приложения.
Несмотря на то, что операции могут относится к разным приложениям, Android поддерживает удобство работы пользователя, сохраняя обе операции в одной задаче. Задача — это коллекция операций, с которыми взаимодействует пользователь при выполнении определенного задания. Начальным местом для большинства задач является главный экран устройства. Когда пользователь нажимает на иконку главного экрана, то эта задача переходит на передний план. Если у приложения нет задач, т.е. приложение не использовалось, то создается новая задача и открывается «основная» операция этого приложения в качестве корневой операции в стеке. Операции упорядочены в стеке (стек переходов назад), в том порядке, в котором они открывались.
Каждая стартуемая операция помещается на вершину стека и получает фокус. Предыдущая операция остается в стеке, но её выполнение приостанавливается, и система «сохраняет» её текущее состояние (интерфейс). При нажатии кнопки «Назад», текущая операция удаляется из вершины стека (операция уничтожается) и возобновляется работа предыдущей операции с восстановлением предыдущего состояния её пользовательского интерфейса. Операции в стеке никогда не переупорядочиваются; выполняется только добавление операций в стек и удаление из него. Т.е. при запуске новой операции, она добавляются в стек, и удаляется из стека, когда пользователь выходит из неё. Таким образом, стек переходов работает по принципу «последним пришел — первым вышел».
Callback методы
Когда Android останавливает операцию по какой-либо причине (например, запуск новой операции), или возобновляет её работу, для уведомления об изменении состояния операции используются callback-методы обратного вызова жизненного цикла операции. Чтобы создать операцию, необходимо сначала создать активность Activity (или её подкласс). В активности можно переопределить (override) методы обратного вызова, которые вызывает система при переходе операции из одного состояния своего жизненного цикла в другое, например при создании, остановке, возобновлении или уничтожении операции. В callback-методах можно выполнить определенные действия, связанные с чтением и освобождением ресурсов.
Класс Activity имплементирует Callback-методы представленные в следующей таблице :
Методы обратного вызова Activity
Метод | Описание |
---|---|
onCreate() | метод вызывается при первом создании Activity |
onStart() | метод вызывается перед тем, как интерфейс Activity будет открыт пользователю |
onResume() | метод вызывается перед предоставлением доступа пользователю к активности |
onPause() | метод вызывается перед открытием другой Activity |
onStop() | метод вызывается после удаления интерфейс Activity с экрана устройства |
onDestroy() | метод вызывается перед уничтожением Activity |
Как видно из описания в таблице callback-методы не вызывают смену состояния активности. Наоборот, при смене состояния Activity система вызывает эти методы, чтобы можно было бы во-время реагировать на это программным способом. Схематично жизненный цикл операции представлен на следующем рисунке, который позаимствован со страницы описания Операций android-приложений.
onCreate
Метод приложения onCreate необходимо переопределить, поскольку система вызывает его при создании активности. В этом методе вызывается setContentView(), определяющий шаблон layout пользовательского интерфейса активности. В реализации метода необходимо инициализировать переменные и загрузить ресурсы, связать данные с элементами управления. Длительные инициализации следует выполнять в фоновом процессе, а не в методе onCreate, поскольку система может вызвать диалоговое окно ANR (Application Not Responding, приложение не отвечает).
В качестве параметра метод onCreate принимает объект Bundle, содержащий состояние пользовательского интерфейса последнего вызова обработчика onSaveInstanceState. Для восстановления интерфейса в его предыдущем состоянии необходимо использовать эту переменную внутри onCreate() или переопределить метод onRestoreInstanceState().
onStart
Метод onStart вызывается либо при создании активности, либо перед возобновлением работы приостановленного приложения. При вызове данного метода интерфейс приложения еще не виден на экране.
onResume
Метод onResume() вызывается после метода onStart(), когда пользователь взаимодействует с окном и приложение получает монопольные ресурсы. Помните, что система вызывает данный метод каждый раз, когда активность переходит на передний план. Таким образом, метод onResume() можно использовать для инициализации компонентов, регистрации любых процессов, которые были освобождены/приостановлены в методе onPause() и выполнить любые другие инициализации, когда Activity вновь активна.
Старайтесь фомрировать относительно быстрый и легковесный код, чтобы приложение было «отзывчивым» при скрытии или появлении на экране.
onPause
При открытии/восстановлении новой/другой активности вызывется метод onPause текущей активности. По сути, если вызывается метод onPause, то происходит свёртывание текущей активности. И поскольку пользователь может назад не вернуться, то обычно именно в этом методе следует сохранить изменения, деактивировать и отпустить монопольные ресурсы, остановить воспроизведение видео, аудио, анимацию и обработку данных от GPS. Исходя из архитектуры своего приложения, можно также приостановить выполнение потоков пока активность вновь не появится на переднем плане.
Старайтесь формировать относительно быстрый и легковесный код, чтобы приложение оставалось отзывчивым при скрытии с экрана или выходе на передний план.
onStop
Когда окно становится невидимым на устройстве вызывается метод onStop. Это может произойти при удалении активности или при старте другой активности, перекрывающей окно текущей. При остановке активности её объекты сохраняются в памяти. Система отслеживает текущее состояние для каждого компонента (View). Поэтому, если пользователь ввёл какой-либо текст в текстовое поле, то его содержание не потеряется и, при восстановлении работы активности, этот текст будет восстанавлен.
Примечание: при закрытии/останове активности состояние объектов хранится в специальном объекте типа Bundle в виде ключ-значение; значения компонентов восстанавливаются при восстановлении состояния активности.
В методе onStop() можно выполнить тяжеловесные операции, связанные с сохранением данных при приостановке сложной анимации, отслеживании показаний датчиков, запросов к GPS, таймеров, сервисов или других процессов, которые нужны исключительно для восстановления пользовательского интерфейса.
При нехватке памяти система может уничтожить скрытую активность вызовом метода onDestroy(), игнорируя метод onStop().
onDestroy
Метод onDestroy вызывается перед завершением работы активности. Это последний вызов системой метода активности перед ее уничтожением. В данном методе, при необходимости, следует проверить сохранение и освобожение используемых ресурсов.
Пример
Для наглядности вышеизложенного создадим пример с переопределением Callback-методов, в которых выведем в журнал Logcat соответствующие сообщения с тегом STATE.
Стартуйте пример и следите за сообщениями. Они будут представлены в определенной последовательности вызовов Сallback-методов в виде сообщений на вкладке Logcat.
Протоколирование
Ниже представлены выполнения определенных действий, сопровождаемых выводом соответствующих сообщений в Logcat.
Полный список
— создадим и вызовем второе Activity в приложении
Урок был обновлен 12.06.2017
Мы подобрались к очень интересной теме. На всех предыдущих уроках мы создавали приложения, которые содержали только один экран (Activity). Но если вы пользуетесь смартфоном с Android, то вы замечали, что экранов в приложении обычно больше. Если рассмотреть, например, почтовое приложение, то в нем есть следующие экраны: список аккаунтов, список писем, просмотр письма, создание письма, настройки и т.д. Пришла и нам пора научиться создавать многоэкранные приложения.
Application/Library name: TwoActivity
Module name: p0211twoactivity
Package name: ru.startandroid.p0211twoactivity
Откроем activity_main.xml и создадим такой экран:
На экране одна кнопка, по нажатию которой будем вызывать второй экран.
Открываем MainActivity.java и пишем код:
Если помните, при создании проекта у нас по умолчанию создается Activity.
От нас требуется только указать имя этого Activity – обычно мы пишем здесь MainActivity. Давайте разбираться, что при этом происходит.
Давайте откроем этот файл:
Нас интересует тег application. В нем мы видим тег activity с атрибутом name = MainActivity. В activity находится тег intent-filter с определенными параметрами. Пока мы не знаем что это и зачем, сейчас нам это не нужно. Забегая вперед, скажу, что android.intent.action.MAIN показывает системе, что Activity является основной и будет первой отображаться при запуске приложения. А android.intent.category.LAUNCHER означает, что приложение будет отображено в общем списке приложений Android.
Android Studio при создании модуля создала MainActivity и поместила в манифест данные о нем. Если мы надумаем сами создать новое Activity, то студия также предоставит нам визард, который автоматически добавит создаваемое Activity в манифест.
Давайте создадим новое Activity
В появившемся окне вводим имя класса – ActivityTwo, и layout – activity_two.
Класс ActivityTwo создан.
В setContentView сразу указан layout-файл activty_two.
Он был создан визардом
Откройте activty_two.xml и заполните следующим кодом:
Экран будет отображать TextView с текстом «This is Activity Two».
Сохраните все. Класс ActivityTwo готов, при отображении он выведет на экран то, что мы настроили в layout-файле two.xml.
Давайте снова заглянем в файл манифеста
Появился тег activity с атрибутом name = .ActivityTwo. Этот тег совершенно пустой, без каких либо параметров и настроек. Но даже пустой, он необходим здесь.
(добавляете только строки 2 и 3)
Обновите импорт, сохраните все и можем всю эту конструкцию запускать. При запуске появляется MainActivity
Нажимаем на кнопку и переходим на ActivityTwo
Код вызова Activity пока не объясняю и теорией не гружу, урок и так получился сложным. Получилось много текста и скриншотов, но на самом деле процедура минутная. Поначалу, возможно, будет непонятно, но постепенно втянемся. Создадим штук 5-6 новых Activity в разных проектах и тема уляжется в голове.
Пока попробуйте несколько раз пройти мысленно эту цепочку действий и усвоить, что для создания Activity необходимо создать класс (который наследует android.app.Activity) и создать соответствующую запись в манифест-файле.
На следующем уроке:
— разбираемся в коде урока 21
— теория по Intent и Intent Filter (не пропустите, тема очень важная)
— немного о Context
Присоединяйтесь к нам в Telegram:
— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование
— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня
— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме
Нестыдные вопросы про жизненный цикл
Каждый разработчик сталкивался с вопросами про жизненный цикл Activity: что такое bind-сервис, как сохранить состояние интерфейса при повороте экрана и чем Fragment отличается от Activity.
У нас в FunCorp накопился список вопросов на похожие темы, но с определёнными нюансами. Некоторыми из них я и хочу с вами поделиться.
1. Все знают, что если открыть второе активити поверх первого и повернуть экран, то цепочка вызовов жизненного цикла будет выглядеть следующим образом:
FirstActivity: onPause
SecondActivity: onCreate
SecondActivity: onStart
SecondActivity: onResume
FirstActivity: onSaveInstanceState
FirstActivity: onStop
SecondActivity: onPause
SecondActivity: onSaveInstanceState
SecondActivity: onStop
SecondActivity: onCreate
SecondActivity: onStart
SecondActivity: onRestoreInstanceState
SecondActivity: onResume
SecondActivity: onPause
FirstActivity: onCreate
FirstActivity: onStart
FirstActivity: onRestoreInstanceState
SecondActivity: onStop
А что будет в случае, если второе активити прозрачное?
Решение
В случае с прозрачным верхним активити с точки зрения логики всё немного отличается. Именно потому, что оно прозрачное, после поворота необходимо восстановить содержимое и того активити, которое находится непосредственно под ним. Поэтому порядок вызовов будет немного отличаться:
FirstActivity: onPause
SecondActivity: onCreate
SecondActivity: onStart
SecondActivity: onResume
SecondActivity: onPause
SecondActivity: onSaveInstanceState
SecondActivity: onStop
SecondActivity: onCreate
SecondActivity: onStart
SecondActivity: onRestoreInstanceState
SecondActivity: onResume
FirstActivity: onSaveInstanceState
FirstActivity: onStop
FirstActivity: onCreate
FirstActivity: onStart
FirstActivity: onRestoreInstanceState
FirstActivity: onResume
FirstActivity: onPause
2. Ни одно приложение не обходится без динамического добавления вью, но иногда приходится перемещать одну и ту же вью между разными экранами. Можно ли один и тот же объект добавить одновременно в два разных активити? Что будет, если я создам её с контекстом Application и захочу добавлять одновременно в различные активити?
Зачем это нужно?
Существуют «не очень приятные» библиотеки, которые внутри кастомных вью держат важную бизнес-логику, и пересоздание этих вью в рамках каждого нового активити является плохим решением, т.к. хочется иметь один набор данных.
Решение
Ничего не мешает создать вью с контекстом Application. Она просто применит дефолтные стили, не относящиеся к какому-либо активити. Также без проблем можно перемещать эту вью между разными активити, но нужно следить, чтобы она была добавлена только в одного родителя
Можно, например, подписаться на ActivityLifecycleCallbacks, на onStop удалять (removeView) из текущего активити, на onStart добавлять в следующее открываемое (addView).
3. Фрагмент можно добавить через add и через replace. А в чём отличие между этими двумя вариантами с точки зрения порядка вызова методов жизненного цикла? В чём преимущества каждого из них?
Решение
Даже если вы добавляете фрагмент через replace, то это не значит, что он полностью заменяется. Это значит, что на этом месте в контейнере заменится его вью, следовательно, у текущего фрагмента будет вызвано onDestroyView, а при возврате назад будет снова вызван onCreateView.
Это довольно сильно меняет правила игры. Приходится детачить все контроллеры и классы, связанные с UI именно в onDestroyView. Нужно чётко разделять получение данных, необходимых фрагменту, и заполнение вью (списков и т.д.), так как заполнение и разрушение вью будет происходить намного чаще, чем получение данных (чтение каких-то данных из БД).
Также появляются нюансы с восстановлениям состояния: например, onSaveInstanceState иногда приходит после onDestroyView. К тому же стоит учитывать, что если в onViewStateRestored пришёл null, то это значит, что не нужно ничего восстанавливать, а не сбрасываться до дефолтного состояния.
Если говорить про удобства между add и replace, то replace экономнее по памяти, если у вас глубокая навигация (у нас глубина навигации юзера — один из продуктовых KPI). Также намного удобнее с replace управлять панелью инструментов, так как в onCreateView можно её переинфлейтить. Из плюсов add: меньше проблем с жизненным циклом, при возврате назад не пересоздаются вью и не нужно ничего заново заполнять.
4. Иногда всё ещё приходится работать напрямую с сервисами и даже с bind-сервисами. С одним из подобных сервисов взаимодействует активити (только один активити). Он коннектится к сервису и передаёт в него данные. При повороте экрана наш активити разрушается, и мы обязаны отбайндится от этого сервиса. Но если нет ни одного соединения, то сервис разрушается, и после поворота bind будет к совершенно другому сервису. Как сделать так, чтобы при повороте сервис оставался жить?
Решение
Если вы знаете красивое решение, то напишите в комментариях. На ум приходит только нечто подобное:
5. Недавно мы переделали навигацию внутри нашего приложения на Single Activity (с помощью одной из доступных библиотек). Раньше каждый экран приложения был отдельным активити, сейчас навигация работает на фрагментах. Проблема возврата к активити в середине стека решалась intent-флагами. Как можно вернуться к фрагменту в середине стека?
Решение
Да, решения из коробки FragmentManager не предоставляет. Cicerone делает внутри себя нечто подобное:
6. Также недавно мы избавились от такого неэффективного и сложного компонента, как ViewPager, потому что логика взаимодействия с ним очень сложна, а поведение фрагментов непрогнозируемо в определённых кейсах. В некоторых фрагментах мы использовали Inner-фрагменты. Что будет при использовании фрагментов внутри элементов RecycleView?
Решение
В общем случае не будет ничего плохого. Фрагмент без проблем добавится и будет отображаться. Единственное, с чем мы столкнулись, — это нестыковки с его жизненным циклом. Реализация на ViewPager управляет жизненным циклом фрагментов посредством setUserVisibleHint, а RecycleView делает всё в лоб, не думая про фактическую видимость и доступность фрагментов.
7. Всё по той же причине перехода с ViewPager мы столкнулись с проблемой восстановления состояния. В случае с фрагментами это реализовывалось силами фреймворка: в нужных местах мы просто переопределяли onSaveInstanceState и сохраняли в Bundle все необходимые данные. При пересоздании ViewPager все фрагменты восстанавливались силами FragmentManager и возвращали свое состояние. Что делать в случае с RecycleView и его ViewHolder?
Решение
«Надо писать всё в базу и каждый раз читать из неё», — скажете вы. Или логика сохранения состояния должна быть снаружи, а список — это просто отображение. В идеальном мире так и есть. Но в нашем случае каждый элемент списка — это сложный экран со своей логикой. Поэтому пришлось изобрести свой велосипед в стиле «сделаем такую же логику, как во ViewPager и фрагменте»:
На Fragment.onSaveInstanceState мы считываем состояние нужных нам холдеров и кладём их в Bundle. При пересоздании холдеров мы достаем сохранённый Bundle и на onBindViewHolder передаём найденные состояния внутрь холдеров:
8. Чем нам это грозит?
Решение
На самом деле, ничего плохого в этом нет. В том же RecycleView хранятся списки из элементов с одинаковыми id. Однако всё-таки есть небольшой нюанс:
Стоит быть внимательнее, если у нас в иерархии есть элементы с одинаковыми id, т.к. возвращается всегда именно первый найденный элемент, и на разных уровнях вызова findViewById это могут быть разные объекты.
9. Вы падаете с TooLargeTransaction при повороте экрана (да, здесь по-прежнему косвенно виноват наш ViewPager). Как найти виновного?
Решение
Ниже пример, как мы достаём состояние фрагментов из Bundle:
Далее мы просто вычисляем размер Bundle и логируем его:
10. Предположим, у нас есть активити и набор зависимостей на нём. При определённых условиях нам нужно пересоздать набор этих зависимостей (например, по клику запустить какой-то эксперимент с другим UI). Как нам это реализовать?
Решение
Конечно, можно повозиться с флагами и сделать это каким-то «костыльным» перезапуском активити через запуск интента. Но на деле всё очень просто — у активити есть метод recreate.
Скорее всего, большая часть этих знаний вам и не пригодится, так как к каждому из них приходишь не от хорошей жизни. Однако некоторые из них хорошо демонстрируют, как человек умеет рассуждать и предлагать свои решения. Мы используем подобные вопросы на собеседованиях. Если у вас есть интересные задачи, которые вам предлагали решить на собеседованиях, или вы сами их ставите, напишите их в комментариях — интересно будет обсудить!