Введение
При программирование для Android есть два основных подхода к управлению состоянием Activity, View или Fragment.
Первый подход — это игнорировать состояние и загружать содержимое заново при каждом создании (поворот экрана, переключение между приложениями).
Второй подход — сохранять и восстанавливать состояние компонентов в соответствии с шаблоном onSaveInstanceState/onRestoreInstanceState.
Первый подход легко реализовать, но пользовательский опыт оставляет желать лучшего.
Второй подход влечет за собой огромное количество проблем. Это реально трудно — поддерживать все компоненты в полной готовности к сохранению/восстановлению и одновременно с этим выполнять в фоне какие-то задачи. Давайте рассмотрим некоторые из проблем, которые при этой возникают.
1. Когда Fragment или Activity запускает фоновую задачу (например, Activity хочет подтянуть содержимое из интернета), результат такой задачи нельзя доставить обратно в Activity, если Activity было уничтожено или пересоздано из-за изменения ориентации экрана или перезапуска процесса.
2. Разрекламированная функция Fragment.setRetainInstanceState(true) тут не поможет, потому что такой Fragment сохраняется только для изменений конфигурации экрана. В добавок, хранение ссылок на Fragment может привести к утечкам памяти.
3. Не существует простого способа проверить, запущена ли уже фоновая задача. Обычный способ — это проверить запускалась ли задача ранее при помощи savedInstanceState, и если не запускалась — тогда запустить фоновую задачу. Но, если фоновая задача была потеряна при пересоздании процесса, она не запустится второй раз, и пользователь получит экран со значком вечного прогресса. Такой глюк — совершенно обычное дело и встречается даже в самих популярных приложениях.
4. Сохранение списка фоновых задач в статических переменных или в объекте Application — хорошая идея, но это на самом деле не спасет, если они будут сброшены при перезапуске процесса из-за нехватки памяти.
Состояние фоновых задач и состояния Activity должны быть согласованны.
Есть ли какое-нибудь решение для всех этих проблем?
Решение
С одной стороны, у нас есть Activity, которая сохраняет свое состояние, а с другой у нас есть статические переменные. Иногда Activity выживает, а иногда выживает процесс и статические переменные.
Решение — это хранить все фоновые задачи в статических переменных и доставлять результаты их выполнения в Activity. Если Activity не находится в активном состоянии (между onResume/onPause), то результаты нужно придержать, пока Activity не активизируется.
Параллельно, нужно сохранять/восстанавливать список фоновых задач для Activity, потому что статические переменные могут быть потеряны из-за перезапуска процесса. Тогда эти фоновые задачи нужно будет перезапустить.
Вот так и появился AsyncBean.
Как использовать AsyncBean
public class YourAsyncTaskBean extends AsyncBean {
YourAsyncTaskBean(<аргументы на ваше усмотрение>) {
}
@Override
protected void run(boolean restart) {
// Запустить фоновую задачу. Когда завершена, фоновая задача
// должна вызвать AsyncBean.deliver() в главном потоке.
deliver();
}
}
// где-то в объявлении activity/fragment/view
YourAsyncTaskBean yourBean;
// где-то в activity/fragment/view onCreate/onRestoreInstanceState
if (savedInstanceState != null)
yourBean = AsyncBean.restoreInstance((AsyncBean)savedInstanceState.getSerializable("yourBean"));
// как запустить
yourBean = new YourAsyncTaskBean(<аргументы на ваше усмотрение>);
yourBean.execute(yourBeanListener);
// код сохранения/подключения/отключения к фоновой задаче
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable("yourBean", yourBean);
}
@Override
protected void onResume() {
super.onResume();
if (yourBean != null)
yourBean.onResume(yourBeanListener);
}
@Override
protected void onPause() {
super.onPause();
if (yourBean != null)
yourBean.onPause();
}
// для получения данных после выполнения фоновой задачи используйте
AsyncBeanListener yourBeanListener = new AsyncBeanListener() {
@Override
public void onAsyncBeanStateChanged(AsyncBean bean, AsyncBeanState state) {
if (bean instanceof YourAsyncTaskBean && state == AsyncBeanState.COMPLETED)
...
}
};
Здесь много вспомогательного кода, так что сделайте базовый класс, который будет управлять всем этим вместо вас, как я сделал это в BaseActivity в демонстрационном приложении.
Демонстрационное приложение
github.com/konmik/AsyncBeanDemo
Присутствует apk, можно сразу запускать. Разрешений не требует.
Этот пример поддерживает две фоновые задачи при поворотах экрана и пересоздании Activity, одновременно показывая индикатор прогресса. Когда процесс уничтожается и Activity пересоздается из сохраненного состояния, фоновые задачи автоматически перезапускаются. Состояние фоновых задач и состояние Activity поддерживаются в согласованном состоянии.
Демонстрационное приложение должно корректно работать при:
1. Пересоздании Activity — а) поверните экран или б) откройте настройки разработчика и включите галочку «Do not keep activities» («Не сохранять операции» на русском). Переключайтесь между приложениями во время выполнения фоновых задач. Выполнение фоновых задач не должно прерываться.
2. Пересоздание процесса — откройте диспетчер задач и нажмите «Очистить память». Переключитесь в демонстрационное приложение, вы увидите, как задачи будут перезапущены.
Заключение
AsyncBean заполняет большую брешь в архитектуре Android-приложения.
Комментарии и предложения — приветствуются!
Автор: JackHexen