Как не потерять связь между фоновыми задачами и Activity

в 6:03, , рубрики: Программирование на Android, Разработка под android
Введение

При программирование для 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

Источник

* - обязательные к заполнению поля