- Полный список
- Paused
- Класс AsyncTask
- Создание новой асинхронной задачи
- Простой пример для знакомства. Кот полез на крышу
- Использование параметров
- Используем второй параметр — промежуточные данные
- Используем первый параметр — входящие данные
- Используем третий параметр — возвращаемые данные
- Метод get()
- Тайм-аут
- Метод cancel() — отменяем задачу
- Текущее состояние задачи
- Поворот экрана
- Запустить несколько задач одновременно
Полный список
— немного теории по Task
— фиксируем Activity в состоянии Paused
В этом уроке узнаем, куда помещается Activity, пока его не видно. И откуда оно достается при нажатии кнопки назад. В хелпе об этом написано достаточно понятно. Я сделаю краткий перевод основной части этого хелпа и использую их схемы.
Мы уже знаем, что приложение может содержать несколько Activity. И что Activity умеет вызывать Activity из других приложений с помощью Intent и Intent Filter. Если вы хотите отправить письмо из вашего приложения, вы вызываете Activity почтовой программы и передаете ей данные. Письмо уходит и вы возвращаетесь в ваше приложение. Создается ощущение, что все это происходило в рамках одного приложения. Такая «бесшовность» достигается за счет того, что оба Activity (ваше и почтовое) были в одном Task.
Прежде, чем продолжу объяснять, хочу сразу привести аналогию, чтобы тему легче было понять. В скобках я буду давать понятия-аналоги из Android.
Механизм организации Activity в Android очень схож по реализации с навигацией в браузере. Вы находитесь в одной вкладке(Task) и открываете страницы (Activity) переходя по ссылкам (Intent). В любой момент можете вернуться на предыдущую страницу, нажав кнопку Назад. Но кнопка Вперед отсутствует, т.к. страница, на которой была нажата кнопка Назад, стирается из памяти. И надо снова нажимать ссылку, если хотим попасть на нее. Если вам надо открыть что-то новое, вы создаете новую вкладку и теперь уже в ней открываете страницы, переходите по ссылкам, возвращаетесь назад. В итоге у вас есть несколько вкладок. Большинство из них на заднем фоне, а одна (активная, с которой сейчас работаете) – на переднем.
В итоге список аналогий браузера и Android таков:
Теперь вам будет более понятен текст про Task.
Task – группа из нескольких Activity, с помощью которых пользователь выполняет определенную операцию. Обычно стартовая позиция для создания Task – это экран Домой (Home).
Находясь в Home вы вызываете какое-либо приложение из списка приложений или через ярлык. Создается Task. И Activity приложения (которое отмечено как MAIN в манифест-файле) помещается в этот Task как корневое. Task выходит на передний фон. Если же при вызове приложения, система обнаружила, что в фоне уже существует Task, соответствующий этому приложению, то она выведет его на передний план и создавать ничего не будет.
Когда Activity_A вызывает Activity_B, то Activity_B помещается на верх (в топ) Task и получает фокус. Activity_A остается в Task, но находится в состоянии Stopped (его не видно и оно не в фокусе). Далее, если пользователь жмет Back находясь в Activity_B, то Activity_B удаляется из Task и уничтожается. А Activity_A оказывается теперь на верху Task и получает фокус.
В каком порядке открывались (добавлялись в Task) Activity, в таком порядке они и содержатся в Task. Они никак специально не сортируются и не упорядочиваются внутри. Набор Activity в Task еще называют back stack. Я буду называть его просто — стэк.
Схема (с офиц.сайта) демонстрирует пример:
В верхней части то, что видит пользователь. В нижней – содержимое Task. Видно, как при вызове новых Activity они добавляются в верх стэка. А если нажата кнопка Назад, то верхнее Activity из стэка удаляется и отображается предыдущее Activity.
Допустим у нас есть Task с несколькими Activity. Он на переднем фоне, мы с ним работаем сейчас.
— если мы нажмем кнопку Home, то ничего не будет удалено, все Activity сохранятся в этом Task-е, а сам Task просто уйдет на задний фон и его всегда можно будет вызвать оттуда, снова вызвав приложение, Activity которого является корневым для Task-а. Либо можно удерживать кнопку Home и мы увидим как раз список Task-ов, которые расположены на заднем фоне.
— если же в активном Task-е несколько раз нажимать кнопку Назад, то в итоге в стэке не останется Activity, пустой Task будет удален и пользователь увидит экран Home.
Там еще как всегда куча нюансов и сложностей, но мы пока остановимся на этом и в дебри не полезем. Этих знаний вполне хватит, чтобы ответить на вопросы предыдущего урока: почему на шаге 2 MainActivity исчезло с экрана, но осталось висеть в памяти и не было уничтожено? Ведь на шаге 3 было уничтожено ActivityTwo после того, как оно пропало с экрана. А на шаге 4 было в итоге уничтожено и MainActivity. Почему шаг 2 стал исключением?
Теперь вы знаете, почему. Потому, что на шаге 2 MainActivity осталось в стэке, а ActivityTwo вставилось на верх стэка и получило фокус. Ну а на шаге 3 и 4 были удалены Activity из верха стэка, в Task не осталось Activity, и мы увидели экран Home.
Если бы мы на шаге 3 нажали не Back, а Home, то Task с обоими Activity ушел бы задний фон и ничего не было бы уничтожено.
Paused
Теперь давайте откроем проект с прошлого урока P0241_TwoActivityState. Мы хотели поймать состояние Paused для Activity. Это состояние означает, что Activity не в фокусе, но оно видно, пусть и частично. Мы можем этого добиться, если присвоим диалоговый стиль для ActivityTwo. Оно отобразится как всплывающее окно и под ним будет частично видно MainActivity – оно и будет в статусе Paused. Давайте реализуем.
Для этого открываем AndroidManifest.xml, вкладка Application, находим там ActivityTwo и справа в поле Theme пишем такой текст: @android:style/Theme.Dialog
Все сохраняем и запускаем приложение.
MainActivity: onCreate()
MainActivity: onStart()
MainActivity: onResume()
MainActivity: onPause()
ActivityTwo: onCreate()
ActivityTwo: onStart()
ActivityTwo: onResume()
Видим, что не был вызван метод onStop для MainActivity, а значит приложение не было переведено в состояние Stopped и находится в режиме Paused.
ActivityTwo: onPause()
MainActivity: onResume()
ActivityTwo: onStop()
ActivityTwo: onDestroy()
MainActivity восстановилось одним лишь вызовом onResume, а onStart не понадобился, т.к. оно было в состоянии Paused, а не Stopped.
Мы четко увидели разницу между этим примером и им же на прошлом уроке. И MainActivity у нас был в состоянии Paused.
Далее можно нажать Back, а можно Home — вы уже знаете, что произойдет в обоих случаях. По логам можно убедиться в этом.
Чтобы вернуть ActivityTwo нормальный режим отображения, зайдите снова в манифест и удалите строку из поля Theme.
Кстати, у вас уже вполне достаточно знаний, чтобы создать приложение с кучей Activity, прописать вызовы и поиграться, посмотреть логи. Тем самым закрепите темы LifeCycle и Task.
На следующем уроке:
— вызываем Activity используя неявный вызов и Intent Filter
Присоединяйтесь к нам в Telegram:
— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование
— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня
— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме
Источник
Класс AsyncTask
Создание новой асинхронной задачи
Класс AsyncTask предлагает простой и удобный механизм для перемещения трудоёмких операций в фоновый поток. Благодаря ему вы получаете удобство синхронизации обработчиков событий с графическим потоком, что позволяет обновлять элементы пользовательского интерфейса для отчета о ходе выполнения задачи или для вывода результатов, когда задача завершена.
Следует помнить, что AsyncTask не является универсальным решением для всех случаев жизни. Его следует использовать для не слишком продолжительных операций — загрузка небольших изображений, файловые операции, операции с базой данных и т.д.
Напрямую с классом AsyncTask работать нельзя (абстрактный класс), вам нужно наследоваться от него (extends). Ваша реализация должна предусматривать классы для объектов, которые будут переданы в качестве параметров методу execute(), для переменных, что станут использоваться для оповещения о ходе выполнения, а также для переменных, где будет храниться результат. Формат такой записи следующий:
Если не нужно принимать параметры, обновлять информацию о ходе выполнения или выводить конечный результат, просто укажите тип Void во всех трёх случаях. В параметрах можно использовать только обобщённые типы (Generic), т.е. вместо int используйте Integer и т.п.
Соответственно, варианты могут быть самыми разными. Вот несколько из них
Для запоминания можно смотреть на схему.
Каркас реализации AsyncTask, в котором используются строковой параметр и два целочисленных значения, нужных для оповещения о выполнении работы и о конечном результате:
У AsyncTask есть несколько основных методов, которые нужно освоить в первую очередь. Обязательным является метод doInBackground(), остальные используются исходя из логики вашего приложения.
- doInBackground() – основной метод, который выполняется в новом потоке. Не имеет доступа к UI. Именно в этом методе должен находиться код для тяжёлых задач. Принимает набор параметров тех типов, которые определены в реализации вашего класса. Этот метод выполняется в фоновом потоке, поэтому в нем не должно быть никакого взаимодействия с элементами пользовательского интерфейса. Размещайте здесь трудоёмкий код, используя метод publishProgress(), который позволит обработчику onProgressUpdate() передавать изменения в пользовательский интерфейс. Когда фоновая задача завершена, данный метод возвращает конечный результат для обработчика onPostExecute(), который сообщит о нём в поток пользовательского интерфейса.
- onPreExecute() – выполняется перед doInBackground(). Имеет доступ к UI
- onPostExecute() – выполняется после doInBackground() (может не вызываться, если AsyncTask был отменен). Имеет доступ к UI. Используйте его для обновления пользовательского интерфейса, как только ваша фоновая задача завершена. Данный обработчик при вызове синхронизируется с потоком GUI, поэтому внутри него вы можете безопасно изменять элементы пользовательского интерфейса.
- onProgressUpdate(). Имеет доступ к UI. Переопределите этот обработчик для публикации промежуточных обновлений в пользовательский интерфейс. При вызове он синхронизируется с потоком GUI, поэтому в нём вы можете безопасно изменять элементы пользовательского интерфейса.
- publishProgress() — можно вызвать в doInBackground() для показа промежуточных результатов в onProgressUpdate()
- cancel() — отмена задачи
- onCancelled() — Имеет доступ к UI. Задача была отменена. Имеются две перегруженные версии.
Вкратце о том, что значит имеет/не имеет доступ к UI. Все ваши кнопки, текстовые метки, ImageView (всё, что отображается на экране) являются частью пользовательского интерфейса — User Interface (UI). Ваша задача — не допустить, чтобы в методе doInBackground() было обращение к какому-нибудь элементу. Например, нельзя установить текст в TextView через метод setText() или поменять цвет шрифта в EditText. В примерах вы увидите, как нужно делать подобные вещи.
На заметку: Несмотря на то, что студия генерирует строки super.onPreExecute() и super.onPostExecute() для соответствующих методов, вы их можете удалить. В исходниках суперкласса методы ничего не делают (это просто заглушка), поэтому во многих примерах в интернете они опущены. Пусть вас это не пугает.
Простой пример для знакомства. Кот полез на крышу
Напишем простой пример с использованием названных методов. Предположим, мы хотим описать процесс восхождения котов на крышу. Всегда оставалось загадкой, как они умудряются залезать на крышу, но, очевидно, что это очень сложная работа, которую удобно использовать в отдельном потоке.
Выведем на экран слово Полез на крышу в методе onPreExecute(), эмулируем тяжёлый код в методе doInBackground(), выведем на экран слово Залез в методе onPostExecute().
Создадим новый проект и добавим на экран кнопку, индикатор прогресса и текстовую метку:
При щелчке кнопки должна запуститься тяжёлая задача. Это может быть загрузка файла из сети, обработка изображения, сохранение больших данных в файл или в базу данных. В нашем случае — кот полез на крышу. В TextView будем выводить текущую информацию. Компонент ProgressBar будет показывать, что приложение не зависло во время выполнения задачи.
Сначала напишем код, а потом будет объяснение к нему.
Запустите проект и нажмите на кнопку. Сначала появится текст «Кот полез на крышу», который через 5 секунд сменится надписью «Кот залез на крышу». Индикаторе прогресса при этом будет постоянно крутиться.
У кота одна задача — залезть на крышу. Поэтому мы создали новую задачу CatTask, которая наследуется от AsyncTask. Для первого примера мы использовали Void, чтобы пока не связываться с параметрами.
В методе onPreExecute() мы устанавливаем начальный текст перед выполнением задачи.
В методе doInBackground() идёт имитация тяжёлой работы. Напоминаю, что здесь нельзя писать код, связанный с пользовательским интерфейсом. Ради интереса поместите в методе строчку:
Студия будет ругаться и не позволит запустить приложение.
В методе onPostExecute() мы выводим сообщение, которое появится после выполнения задачи.
Обратите внимание, что если мы нажмём на кнопку, пока работает AsyncTask, то создастся и запустится новая задача поверх старой. Получается, что две задачи будут одновременно работать с экраном активности. Нужно избегать подобных ситуаций. Позже я покажу, как это сделать.
На данный момент для избежания конфликтов можно скрыть кнопку в методе onPreExecute() и показать её снова в onPostExecute():
Сама задача CatTask (объект AsyncTask) создаётся в UI-потоке. Также в UI-потоке вызывается метод объекта execute()
Кстати, коты прислали фотографию, которая объясняет, зачем они лезут на крышу. Интересно, откуда они узнали про эту статью?
Использование параметров
Для первого примера мы не использовали параметры. Наша задача была показать сообщения до и после выполнения задачи. Но часто нам необходимо знать промежуточные результаты выполняемой задачи. Например, мы хотим знать, что кот сейчас на втором этаже и ему осталось ещё двенадцать (бедный кот).
Когда мы создавали свой класс CatTask, то использовали угловые скобки, в которых необходимо указать три типа данных:
- Тип входных данных
- Тип промежуточных данных, которые используются для понимания
- Тип возвращаемых данных
В первом примере мы указали . Эта запись означает, что мы не будем использовать параметры.
Во втором примере попробуем воспользоваться ими.
Используем второй параметр — промежуточные данные
Начнём со второго параметра, который отвечает за промежуточные данные. Итак, нам нужно знать текущий этаж, на котором находится кот. Если дом 14-этажный, то у нас должны быть значения от 1 до 14 типа Integer:
Метод onPreExecute() оставляем без изменений, здесь мы просто выводим сообщение о начале штурма здания.
Переходим к методу doInBackground(). Мы ещё не используем входящие данные, поэтому пока здесь используется Void. В самом методе создаётся цикл от 0 до 14 и при каждом проходе цикла увеличивается счётчик counter на единицу.
В методе doInBackground() вызываются два метода. Первый метод getFloor() — наш. Здесь мы реализуем свою логику тяжёлой работы. Воспринимайте его как эмуляцию загрузки файлов или другую сложную работу. В нашем случае при покорении очередного этажа кот должен пометить его, нацарапать на стене неприличное слово из трёх букв типа МЯУ или МУР. В вашем приложении возможно это будет код обработки большой картинки, скачанной по сети. Пока у нас здесь просто пауза на одну секунду.
Второй метод publishProgress() — системный метод. Когда мы в методе doInBackground() вызываем метод publishProgress() и передаём туда данные, то срабатывает метод onProgressUpdate(), который получает эти данные. Тип принимаемых данных равен второму типу из угловых скобок, у нас это Integer. Метод onProgressUpdate() используется для вывода промежуточных результатов и имеет доступ к UI. Таким образом, метод publishProgress() является своеобразным мостиком для передачи данных из doInBackground() в onProgressUpdate(). Мы передаём значение счётчика, которое выводится в текстовой метке.
При запуске проекта, вы увидите, как в текстовом поле будут меняться числа от 0 до 14.
Для наглядности можно добавить на экран горизонтальный ProgressBar, который будет показывать в удобном виде график прохождения этажей.
Полностью код будет следующим:
Используем первый параметр — входящие данные
Теперь попробуем задействовать первый параметр, который отвечает за входящие данные. На практике это может быть список адресов, с которых надо загрузить картинки или что-то в этом роде.
Изменим объявление метода doInBackground():
У метода есть входные параметры типа String. Сразу поменяем первый параметр в классе CatTask:
Теперь нужно передать в метод execute() список адресов файлов для загрузки. Метод doInBackground принимает эти данные и в цикле по одному загружает эти файлы. Дальше без изменений. После прохождения каждого этажа (загрузки каждого файла) вызывается метод publishProgress(), в который передаются данные.
В обработчике кнопки вызовем метод execute(), которому передадим набор строк, так как мы указали этот тип в угловых скобках на первом месте.
Полностью код будет следующим:
Показать код (щелкните мышкой)
В нашем примере мы не загружаем картинки, поэтому строки с адресами изображений не задействованы. В других примерах вам будет попадаться такой код.
Ещё раз закрепим материал.
Метод execute() вызывается в основном потоке, чтобы начать выполнение задачи. В него можно передать набор данных определенного типа. Если что-то передаём, то этот тип будет указан первым в угловых скобках при создании наследника AsyncTask.
Методы onPreExecute() и onPostExecute() вызываются системой в начале и конце выполнения задачи.
Основной метод для тяжёлой работы — doInBackground(). Можно передать методу какие-то данные. В этом случае смотрим на execute().
Метод publishProgress() нужен в том случае, когда требуется обрабатывать промежуточные данные. Его нужно явно вызвать в методах doInBackground(), onPreExecute() или onPostExecute(). На вход передаём промежуточные данные определенного типа. Этот тип указан вторым в угловых скобках при описании AsyncTask.
Метод onProgressUpdate() получает на вход промежуточные результаты от метода publishProgress(). Так как метод onProgressUpdate() принимает на вход набор параметров, то при передаче одного значения от publishProgress нужно взять первый элемент массива.
Используем третий параметр — возвращаемые данные
Осталось рассмотреть вариант использования третьего параметра.
В третьем параметре указывается тип объекта, который должен вернуться из AsyncTask. Получить его можно двумя способами:
- Он передаётся методу onPostExecute(), который вызывается по окончании задачи
- Существует метод get(), способный возвращать нужный объект
Если мы собираемся что-то возвращать, то это надо указать в возвращаемом значении у метода doInBackground() и в входящем параметре для метода onPostExecute().
Начнём с метода doInBackground() и изменим его описание следующим образом:
Как видите, наш метод теперь возвращает тип Integer. Пусть это будет число 2012 — напоминание про обман племени майя, которые обещали в том году конец света. Даже коты ждали его.
Студия подчеркнёт исправленный вариант, указывая несоответствие в параметрах у класса AsyncTask. Исправим:
Теперь обратимся к методу onPostExecute(). Тип Integer должен стать входящим параметром. Поменяем объявление метода:
В нашем случае входящий параметр не несёт смысловой нагрузки, просто выведем значение в текстовом поле.
Полный код для самопроверки:
Показать код (щелкните мышкой)
Метод get()
Выше уже упоминалось, что метод get() возвращает значение задачи. Тут следует рассмотреть два момента. Первый момент — вызовем метод после выполнения задачи. Второй момент — попробуем получить значение во время выполнения задачи.
Начнём с первого варианта. Добавим на экран активности ещё одну кнопку buttonResult с надписью «Получить результат»:
Добавим код для обработчика кнопки:
Запустите приложение, далее запустите задачу нажатием первой кнопки, дождитесь окончания задачи и нажмите вторую кнопку. Во всплывающем сообщение вы увидите то же число «2012», который возвращает метод onPostExecute().
А что случится, если задача находится в стадии выполнения, а мы вызовем метод get()? Давайте проверим.
Запустим снова приложение, нажмём на первую кнопку для запуска задачи и, не дожидаясь окончания задачи, нажмите на вторую кнопку.
Создаётся ощущение, что программа остановилась. ProgressBar перестанет обновляться, текстовые сообщения также не выводятся в TextView. Но после окончания задачи появится текст и вернётся результат. Что же произошло?. Метод get() блокирует основной UI-поток и ждёт завершения AsyncTask. Это продолжается до тех пор, пока не выполнится код в методе doInBackground(). После этого метод get() возвращает результат и освобождает поток.
Таким образом метод блокирует поток, в котором он выполняется, и не отпустит, пока не получит какой-то результат или не выскочит исключение.
Тайм-аут
Есть ещё реализация метода с тайм-аутом. В этом случае метод get() будет ждать указанное время, а потом сгенерирует Exception. Если же задача уже была завершена, то метод выполнится сразу и ждать ничего не будет.
Перепишем код для второй кнопки:
Смотрим, что получилось. Приложение снова подвисает, но через секунду оживает и продолжает работу. Метод get() ждёт одну секунду, и если не получает результат, то генерирует исключение TimeoutException. Мы обрабатываем исключение и выводим в лог соответствующее сообщение. А приложение продолжит выполнять задачу и после успешной обработки метода onPostExecute() выводит результат.
Метод cancel() — отменяем задачу
Метод cancel() позволяет указать на отмену уже выполняющейся задачи. У метода один boolean-параметр, который указывает, может ли система прервать выполнение потока.
Кроме того, в методе doInBackground() можно проверять метод isCancelled(). Как только мы выполним метод cancel(), метод isCancelled() будет возвращать true и мы должны завершить метод doInBackground(). Таким образом, метод cancel() служит своеобразной меткой, что задачу нужно отменить. А метод isCancelled() будет считывать результат предыдущего метода и позволет выполнит код для завершения задачи.
Вернёмся к нашей истории с упрямым котом, который лезет на крышу. Оставим на экране две кнопки для запуска и отмены задачи. Саму задачу немного упростим, убрав отвлекающий код (используем один из первых вариантов примера в начале статьи).
Когда мы нажимаем на кнопку «Отменить операцию», то в методе cancel() используем параметр, равный true. В методе doInBackground() при работе цикла идёт проверка отмены (метод isCancelled()). Если приложение видит, что пользователь выбрал отмену задачи, то вместо метода onPostExecute() вызывается метод onCancelled(), в котором и прописываем свою логику кода.
В Android 4.0 появился ещё один метод onCancelled(Void result), способный принимать результат от метода doInBackground().
Текущее состояние задачи
Мы можем определить, в каком состоянии сейчас находится задача. Существует три состояния:
- PENDING – задача не запущена
- RUNNING – задача выполняется
- FINISHED – задача успешно завершена. Метод onPostExecute() отработал
Вы можете самостоятельно проверить, в каком состоянии находится ваша задача, добавив код в нужном месте:
Вкратце, когда вы создаёте задачу (new CatTask()), то состояние PENDING. Когда вы запускаете задачу (execute()) и задача работает, то состояние RUNNING. Когда задача завершится, то состояние FINISHED. Если вы отменили задачу, то состояние по-прежнему будет RUNNING, поэтому используйте проверку через метод isCancelled() для более точного определения состояния:
В моих опытах после отмены сначала показывало состояние RUNNING, а после повторной проверки уже показывало FINISHED, возможно состояние не сразу устанавливается.
Поворот экрана
Как вы знаете, при повороте экрана активность пересоздаётся. А что происходит с задачей? При повороте старые объекты будут потеряны, в том числе и ссылка на нашу задачу. Фактически получается, что при повороте создаётся ещё одна задача, которая начинает выполняться с самого начала, при этом где-то выполняется и старая задача.
Ситуация не совсем приятная. Для ознакомления с решениями этой задачи рекомендую почитать статью Урок 91. AsyncTask. Поворот экрана, а заодно и другие материалы с сайта, связанные с AsyncTask.
Запустить несколько задач одновременно
AsyncTask в старых устройствах не поддерживал параллельное выполнение. Позже стало возможным запускать до пяти задач. Оставляю на память.
Расширенный пример. Подготовим макет экрана из пяти индикаторов прогресса.
Запускаем первый индикатор, и параллельно с ним четвёртый и пятый при помощи executeOnExecutor(). Второй и третий индикатор начнут действовать после окончания работы всех предыдущих задач.
Источник