Введение
При разработке практически любого приложения с пользовательским интерфейсом, программист рано или поздно встречается с ситуацией когда нужно выполнить долговременную операцию. Во время долговременной операции обычно пользователю показывают окно «Пожалуйста, подождите...» или что то в этом роде.
Платформа Android, да и наверное многие другие платформы не позволяют выполнять долговременные операции в UI потоке. Выполняя долговременную операцию в UI потоке вы просто напросто повесите программу.
Android предлагает для решения такого рода задач AsyncTask. AsyncTask позволяет выполнять долговременную операцию и взаимодействовать с UI потоком.
Проблема
Казалось бы ничего сложного, создаем AsyncTask передаем созданному AsyncTask указатель на текущую Activity и все готово, фоновый процесс работает, обновляет UI, все счастливы.
Все прекрасно работает до тех пор, пока не сменится ориентация экрана (Книжная > Альбомная, Албомная > Книжная) или приложение не будет отправлено в фон. Обычно при таком подходе после смены ориентации экрана происходит краш приложения.
Почему происходит краш приложения
Потому, что при смене ориентации эрана Android пересоздает Activity, в итоге Activity на который вы передавали ссылку AsyncTask-у уже уничтожен и ваш AsyncTask пытается взаимодействовать с уничтоженным объектом.
Решения
Все решают проблему по разному, не буду описывать все предлагаемые методы решения, их вы можете найти погуглив или в различных блогах или на StackOverflow.
Каждый из подходов требует реализации определенных твиков, дополнительных тестов, отладки, написания дополнительного кода или ограничения возможностей пользовательского интерфейса.
Столкнувшись с этой проблемой меня сильно раздражало, что для такой тривиальной задачи нет готового коробочного решения (во всяком случае я не нашел).
Можно я попробую
Не судите, пожалуйста, строго и не сочтите за самопиар, я просто хочу написать как я решаю данную проблему и поделиться решением с другими.
Я написал некоторое подобие фреймворка (я считаю, что фреймворк это слишком громко для моего решения, но ввиду особенностей использования все же подобие фреймворка) и разместил его на Github.
Исходные коды открыты, лицензия Apache 2.0, подчеркну еще раз, что я ничего не продаю, не навязываю и не попрашайничаю. Фреймворк называется Asmyk.
Как работает
При активации Activity (событие onResume), Activity отмечается в контексте приложения. Далее фоновая задача адресует UI задачи Activity из контекста.
Да ничего сложного, однако при реализации данной схемы начинающему программисту может быть немного сложновато и придется потратить пару дней на реализацию качественного решения.
Как внедрить
Скачиваем AAR файл и подключаем к проекту.
Если вы не расширяете класс Application своей реализацией, укажите в файле AndroidManifest.xml в теге application атрибут:
android:name="net.mabramyan.asmyk.core.AsmykApplicationContext"
Пример:
<application
...
android:name="net.mabramyan.asmyk.core.AsmykApplicationContext"
...
>
...
Если вы расширяете класс Application своей реализацией, необходимо наследовать свою реализацию от класса AsmykApplicationContext.
Activity с которыми вы будете работать из фона должны наследоваться от AsmykCompatActivitiy.
Примечание: Можете наследовать хоть все свои Activity от AsmykCompatActivitiy.
Пример
UI «Пожалуйста, подождите...»
Создаете Activity наследуемую от AsmykPleaseWaitActivity или от AsmykBasicPleaseWaitActivity.
Что нужно реализовать для AsmykPleaseWaitActivity
Метод описывает действия которые должна выполнить Activity при обновлении статуса операции. Объект progressObj будет передан из AsmykPleaseWaitTask:
void onProgress(final Object progressObj)
Метод описывает действия которые должна выполнить Activity при провале операции. Объект errorObj будет передан из AsmykPleaseWaitTask.
void onFail(final Object errorObj)
Метод описывает действия которые должна выполнить Activity при успешном выполнении операции. Объект successObj будет передан из AsmykPleaseWaitTask.
void onSuccess(final Object successObj)
Что нужно реализовать для AsmykBasicPleaseWaitActivity
Метод описывает действия которые должна выполнить Activity при успешном выполнении операции. Объект successObj будет передан из AsmykPleaseWaitTask:
void onSuccess(final Object successObj)
Методы onFail и onProgress уже реализованы. В качестве аргументов принимают String с описанием ошибки или с описанием нового состояния фоновой задачи соответственно.
Фоновая задача
Далее создаем фоновую задачу реализуя класс AsmykPleaseWaitTask. Вам придется реализовать всего один метод описывающий вашу фоновую задачу:
void doInBackground(final AsmykApplicationContext ctx)
В процессе выполнения задачи вы можете вызывать метод
void fireProgress(AsmykApplicationContext ctx, final Object progressObj)
— данный метод впоследствии вызовет onProgress у вашей AsmykPleaseWaitActivity. По завершении задачи вызовите метод fireSuccess или fireFailed в зависимости от результата выполнения операции.Запуск
Пример вызова фоновой задачи:
pleaseWaitTask.start((AsmykApplicationContext) MainActivity.this.getApplicationContext());
Intent intent = new Intent(MainActivity.this, PleaseWaitActivity.class);
startActivity(intent);
Внимание: AsmykPleaseWaitTask реально начнет выполнение задачи только после того, как будет показана Activity наследуемая от AsmykPleaseWaitActivity.
Это сделано для того, что бы фоновая задача не завершилась раньше чем будет показана Activity.
Пример простенького приложения вы можете посмотреть тут.
Обращение к комментирующим
Спасибо за то, что прочли и нашли время прокомментировать.
Я за критику если она конструктивная.
Для того, что бы пост был полезным и для других читателей, давайте сделаем так:
Если вы считаете какие либо конструкции кода не правильными, опишите почему.
Если вы предлагаете метод который удобнее или правильнее чем вышеописанный, прикладывайте ваше решение применительно к задаче (это основная задача которую решает вышеописанный фреймворк):
1) Мы уже получили от пользователя порцию данных для регистрации, пользователь нажимает «Продолжить»
2) Выполняем шаг регистрации 1. Последовательно отправляется N различных запросов. Тут я хочу показывать пользователю, что именно сейчас программа делает. При этом поворот экрана и т.п. не должны обрывать процесс регистрации.
С моей точки зрения, таких задач может быть множество в реальном приложении.
P.S. Я полагаю, если мы будем прикладывать реальные примеры, пост будет максимально полезным.
У меня все! Спасибо за внимание.
Комментарии (29)
jatx
10.06.2017 13:54-3Ну и еще обычно делают так.
В конструкторе AsynkTask:
this.activityRef = new WeakReference<SomeActivity>(activity);
В начале метода onPostExecute:
SomeActivity activity = activityRef.get(); if (activity==null) return;
afeozzz
10.06.2017 14:05+5Все очень плохо.
Ну вынесите вы долгую операцию в сервис
зачем городить такое?
//waiting for our form while (!(ctx.getCurrentActivity() instanceof AsmykPleaseWaitActivity)) { try { Thread.sleep(500); } catch (InterruptedException e) { return; } }
mabramyan
10.06.2017 18:29Спасибо за критику, данная конструкция добавлена для того, что бы не получилось так, что фоновая задача выполнилась быстрее чем запустилась Activity, например если произошел какой либо непредвиденный сбой в начале операции.
Опишите, пожалуйста, ваше предложение подробнееthelongrunsmoke
11.06.2017 07:21+1Строго говоря, если разработчику нужны длительные операции в активити, фрагменте или вью — он нарушает SRP. Всё это нужно вынести в сервис, а в отображении только проверять состояние, нужна эта операция, работает или уже завершена.
mabramyan
11.06.2017 10:45Ок, давайте на практическом примере.
Моя задача, реализация процесса регистрации:
1) Получаю от пользователя первую порцию данных
2) Выполняю шаг регистрации 1. Последовательно отправляется N различных запросов. Тут я хочу показывать пользователю, что именно сейчас программа делает
3) Получаю от пользователя вторую порцию данных
4) Выполняю шаг регистрации 2. Последовательно отправляется M различных запросов. Тут я хочу показывать пользователю, что именно сейчас программа делает
Как я должен это сделать при помощи сервисов?thelongrunsmoke
11.06.2017 11:26Запросто.
1) Создаёте активность, которая проверяет состояние модели и отображает фрагмент соответствующий этапу регистрации.
2) Фрагмент спрашивает сервис работает ли его задача, и если работает отображает соответствующее уведомление, если нет — ожидает действий пользователя.
Всё. Состояния легко добавляются и однозначны. Кроме того, возможны любые перерывы в работе с приложением.
Да, я знаю что многие любят хранить состояние отображения в самом отображении. Это создаёт много проблем при поддержке.
В целом, создание ниток внутри пределов объекта и принадлежащих только создавшему объекту — жизнеспособная идея… в рамках агентного подхода. Но отображение на роль агента подходит плохо.
Кроме того, существуют лоадеры. Они продолжают работать пока фрагмент не был убит.
pontifex024
10.06.2017 14:38А почему именно в onResume()? Этот метод вызывается довольно часто. Почему не регистрируете в onStart()?
mabramyan
10.06.2017 19:04Для того, что бы фреймворк знал, какая именно Activity сейчас работает.
У фреймворка есть возможность запускать задачи UI при выполнении предусловий см https://mkabramyan.github.io/Asmyk/net/mabramyan/asmyk/core/AsmykUITask.html
Например вы можете выполнить какое либо UI задание из фоновой задачи/сервиса под конкретную Activity или при каких либо специфичных условиях (одно ограничение, проверка должна быть относительно быстрой).
Предусловия описываются в методе isApplicatble()
Запустить задачу вы можете вызвав метод runUITask экземпляра класса AsmykApplicationContext.
Если вы укажете параметр postpone=true, тогда задача будет выполнена если isApplicatble вернет true.
Если isApplicable вернет false задача будет отложена. Как только сменится текущая Activity проверка isApplicatble будет вызвана снова и так до тех пор пока isApplicatble не вернет true. Это позволяет однозначно выполнить специфичную UI задачу из фоновой операции.
Спасибо за вопрос, надеюсь у меня получилось описать понятно
KursoRUS
10.06.2017 18:20+3После того как узнал о Moxy (https://github.com/Arello-Mobile/Moxy) вообще перестал беспокоиться о повороте экрана. В ней презентер спокойно переживает смену конфигурации и плюсом после пересоздания view применяет к нему все вызванные до поворота методы, что позволяет почти не париться о сохранении состояния.
Kalobok
10.06.2017 19:41+1Гораздо удобнее делать такие вещи по инструкции, через Fragment.setRetainInstance(true).
Valle
10.06.2017 21:47А просто лоадер взять не? Примеры лоадеров в документации конечно страшные, но это просто обычный POJO с жизненным циклом activityrecord. Ну или фрагмента.
petrovichtim
11.06.2017 07:47Денис Неклюдов говорил, что у лоадеров есть баг, когда он не завершится никогда.
Valle
11.06.2017 08:45Основные лоадеры ничего не делают сами по себе, это просто объекты которые хранятся с временем жизни активитирекорда, что напрограммируете, то и получится. Я так понимаю, речь про asynctaskloader, можно тогда ссылочку на баг?
BeeJay
11.06.2017 10:41public class AsmykApplicationContext extends Application { ... private Activity currentActivity; ... /** * Internal method * * @param currentActivity */ protected synchronized void setCurrentActivity(AsmykCompatActivity currentActivity) { this.currentActivity = currentActivity; executeUITasks(); } /** * Get current activity * * @return */ public synchronized Activity getCurrentActivity() { return currentActivity; } }
Не делайте такmabramyan
11.06.2017 10:41Почему?
Valle
11.06.2017 20:56+ «current Activity» вообще-то может быть больше одной штуки. + instantrun наверное на этом сильно поперхнется и не обновит ссылку.
mabramyan
11.06.2017 22:42+ «current Activity» вообще-то может быть больше одной штуки
Я вызываю setCurrentActivity в событии onResume. Каким образом их будет несколько?
Это одна из самых крутых утечек памяти из возможных на андроиде.
Я вызываю setCurrentActivity(null) на каждом onPause
Каким образом утечет память? См https://developer.android.com/guide/components/activities/activity-lifecycle.html
instantrun наверное на этом сильно поперхнется и не обновит ссылку
Почему?
Valle
11.06.2017 23:39Я вызываю setCurrentActivity в событии onResume. Каким образом их будет несколько?
Навскидку — multiwindow.
Я вызываю setCurrentActivity(null) на каждом onPause
Ну, к примеру, закрывается ActivityA и открывается ActivityB. Есть вероятность, что ActivityA.onPause() вызовется после ActivityB.onResume(). Тут получится что currentActivity null. Да, утечки памяти тут не будет, но все же нехорошо хранить ссылки на активити в контексте процесса.
Почему?
Ну тут наверное андроид студия показывает предупреждение
«Do not place Android context classes in static fields; this is a memory leak and also breaks Instant Run.» не так ли?mabramyan
12.06.2017 00:081) При компилляции проекта, студия не ругается.
2) Где вы нашли статическое поле?
mabramyan
12.06.2017 11:33Навскидку — multiwindow.
См жизненный цикл multiwindow https://developer.android.com/guide/topics/ui/multi-window.html?hl=ru
Фреймворк не будет лагать в таком режиме. Просто UI задачи предназанченные для специфичной Activity будут выполняться только тогда, когда Activity будет реально актинвной. ИМХО случай довольно редкий, к тому же не вызовет ошибок. Помечу, эту особенность в Javadoc-е
но все же нехорошо хранить ссылки на активити в контексте процесса.
Почему?Valle
12.06.2017 21:41Хорошо, мне ответить нечего больше :-) Основная моя идея что для контроля жизненного цикла активити и фрагментов лучше использовать предназначенные для этого стандартные компоненты, такие как лоадеры или setretaininstance когда надо. Если процесс не принадлежит одной активити то только тогда следует использовать что-то внешнее, например сервисы. Использовать максимально возможный scope — application/process можно, но только если понимаешь что делаешь. Из возможных проблем — wakelocks, doze mode, или неправильное понимание жизненного цикла фрагментов.
Revertis
12.06.2017 09:08Проще всего при показе ProgressDialog (не важно как реализуется) закреплять текущее положение активити, чтобы юзер не мог его изменить. После закрытия ProgressDialog открепляем положение и все довольны.
VNavigator
12.06.2017 14:42<activity android:configChanges="keyboardHidden|orientation|screenSize" ... />
так не пробовал никто?awsi
12.06.2017 18:32Поворот экрана — это не единственная возможная смена конфигурации.
jatx
А просто обрабатывать onConfigurationChanged() не вариант?