Фоновая задача, диалог прогресса, изменение ориентации - есть ли 100% работающее решение?

235

Я загружаю некоторые данные из Интернета в фоновом режиме (я использую AsyncTask) и отображаю диалоговое окно прогресса во время загрузки. Ориентация меняется, действие перезапускается, и затем мой AsyncTask завершается - я хочу закрыть диалоговое окно progess и начать новое действие. Но вызов dismissDialog иногда вызывает исключение (возможно, потому что действие было уничтожено, а новое действие еще не было запущено).

Каков наилучший способ решения этой проблемы (обновление пользовательского интерфейса из фонового потока, которое работает, даже если пользователь меняет ориентацию)? Кто-то из Google предоставил какое-то "официальное решение"?

fhucho
источник
4
Мой пост в блоге на эту тему может помочь. Речь идет о сохранении долгосрочных задач при изменениях конфигурации.
Алекс Локвуд
1
Этот вопрос также связан.
Алекс Локвуд
Просто FTR, здесь есть связанная загадка .. stackoverflow.com/q/23742412/294884
Толстяк

Ответы:

336

Шаг 1: Сделайте AsyncTaskна staticвложенный класс, или совершенно отдельный класс, только не внутренний (не статический вложенный) класс.

Шаг № 2: AsyncTaskУдерживайте Activityэлемент via с помощью элемента данных, задайте его с помощью конструктора и установщика.

Шаг № 3: При создании AsyncTask, поставьте ток Activityконструктору.

Шаг № 4: В onRetainNonConfigurationInstance(), верните AsyncTask, после отсоединения его от первоначальной, теперь уходящей деятельности.

Шаг № 5: onCreate()Если getLastNonConfigurationInstance()нет null, приведите его к своему AsyncTaskклассу и вызовите своего сеттера, чтобы связать вашу новую деятельность с задачей.

Шаг № 6: Не ссылаться на член данных деятельности из doInBackground().

Если вы будете следовать приведенному выше рецепту, все будет работать. onProgressUpdate()и onPostExecute()приостановлены между началом onRetainNonConfigurationInstance()и концом последующего onCreate().

Вот пример проекта, демонстрирующего технику.

Другой подход состоит в том, чтобы бросить AsyncTaskи переместить свою работу в IntentService. Это особенно полезно, если выполняемая работа может быть длительной и продолжаться независимо от действий пользователя (например, загрузка большого файла). Вы можете использовать упорядоченную трансляцию, Intentчтобы действие реагировало на выполняемую работу (если оно все еще находится на переднем плане), или поднимало, Notificationчтобы сообщить пользователю, была ли работа выполнена. Вот сообщение в блоге с большим количеством об этом образце.

CommonsWare
источник
8
Большое спасибо за ваш отличный ответ на этот общий вопрос! Просто чтобы быть точным, вы можете добавить к шагу # 4, что мы должны отсоединить (установить в нуль) действие в AsyncTask. Это хорошо иллюстрируется в примере проекта.
Кевин Гаудин
3
Но что, если мне нужен доступ к участникам Activity?
Евгений
3
@Andrew: создайте статический внутренний класс или что-то, что содержит несколько объектов, и верните его.
CommonsWare
11
onRetainNonConfigurationInstance()устарела и предлагает использовать альтернативу setRetainInstance(), но она не возвращает объект. Можно ли справиться asyncTaskс изменением конфигурации с помощью setRetainInstance()?
Индрек Кюэ
10
@SYLARRR: Абсолютно. Иметь FragmentУДЕРЖИВАЙТЕ AsyncTask. Имеют Fragmentвызов setRetainInstance(true)на себя. Иметь AsyncTaskтолько поговорить с Fragment. Теперь, при изменении конфигурации, Fragmentон не уничтожается и не воссоздается (даже при наличии действия), и поэтому AsyncTaskон сохраняется при изменении конфигурации.
CommonsWare
13

Принятый ответ был очень полезен, но в нем нет диалогового окна прогресса.

К счастью для вас, читатель, я создал чрезвычайно полный и работающий пример AsyncTask с диалоговым окном прогресса !

  1. Вращение работает, и диалог выживает.
  2. Вы можете отменить задачу и диалог, нажав кнопку «Назад» (если вы хотите, чтобы это поведение).
  3. Используются фрагменты.
  4. Расположение фрагмента под действием изменяется должным образом, когда устройство вращается.
Timmmm
источник
Принятый ответ о статических классах (не членах). И это необходимо, чтобы избежать того, что AsyncTask имеет (скрытый) указатель на экземпляр внешнего класса, который становится утечкой памяти при уничтожении действия.
Bananeweizen
Да, я не уверен, почему я добавил это к статическим элементам, поскольку я также использовал их ... странно. Отредактированный ответ.
Timmmm
Не могли бы вы обновить свою ссылку? Мне это действительно нужно.
Ромен Пеллерен
Извините, не удалось восстановить мой сайт - я сделаю это в ближайшее время! Но в то же время он в основном совпадает с кодом в этом ответе: stackoverflow.com/questions/8417885/…
Timmmm
1
Ссылка является фиктивной; просто приводит к бесполезному индексу без указания того, где находится код.
FractalBob
9

Я трудился целую неделю, чтобы найти решение этой дилеммы, не прибегая к редактированию файла манифеста. Предположения для этого решения:

  1. Вам всегда нужно использовать диалог прогресса
  2. Только одна задача выполняется одновременно
  3. Задача должна сохраняться при повороте телефона, а диалоговое окно прогресса автоматически закрывается.

Реализация

Вам нужно будет скопировать два файла, найденные внизу этого поста, в ваше рабочее пространство. Просто убедитесь, что:

  1. Все ваши Activitys должны расширятьсяBaseActivity

  2. В onCreate(), super.onCreate()должна вызываться после инициализации любых членов , которые нужно получить доступ к вашим ASyncTaskс. Кроме того, переопределите, getContentViewId()чтобы предоставить идентификатор макета формы.

  3. Переопределите, onCreateDialog() как обычно, для создания диалогов, управляемых действием.

  4. Посмотрите код ниже для примера статического внутреннего класса, чтобы сделать ваши AsyncTasks. Вы можете сохранить свой результат в mResult для доступа позже.


final static class MyTask extends SuperAsyncTask<Void, Void, Void> {

    public OpenDatabaseTask(BaseActivity activity) {
        super(activity, MY_DIALOG_ID); // change your dialog ID here...
                                       // and your dialog will be managed automatically!
    }

    @Override
    protected Void doInBackground(Void... params) {

        // your task code

        return null;
    }

    @Override
    public boolean onAfterExecute() {
        // your after execute code
    }
}

И наконец, чтобы запустить новое задание:

mCurrentTask = new MyTask(this);
((MyTask) mCurrentTask).execute();

Это оно! Я надеюсь, что это надежное решение поможет кому-то.

BaseActivity.java (организуйте импорт самостоятельно)

protected abstract int getContentViewId();

public abstract class BaseActivity extends Activity {
    protected SuperAsyncTask<?, ?, ?> mCurrentTask;
    public HashMap<Integer, Boolean> mDialogMap = new HashMap<Integer, Boolean>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(getContentViewId());

        mCurrentTask = (SuperAsyncTask<?, ?, ?>) getLastNonConfigurationInstance();
        if (mCurrentTask != null) {
            mCurrentTask.attach(this);
            if (mDialogMap.get((Integer) mCurrentTask.dialogId) != null
                && mDialogMap.get((Integer) mCurrentTask.dialogId)) {
        mCurrentTask.postExecution();
            }
        }
    }

    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
    super.onPrepareDialog(id, dialog);

        mDialogMap.put(id, true);
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        if (mCurrentTask != null) {
            mCurrentTask.detach();

            if (mDialogMap.get((Integer) mCurrentTask.dialogId) != null
                && mDialogMap.get((Integer) mCurrentTask.dialogId)) {
                return mCurrentTask;
            }
        }

        return super.onRetainNonConfigurationInstance();
    }

    public void cleanupTask() {
        if (mCurrentTask != null) {
            mCurrentTask = null;
            System.gc();
        }
    }
}

SuperAsyncTask.java

public abstract class SuperAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
    protected BaseActivity mActivity = null;
    protected Result mResult;
    public int dialogId = -1;

    protected abstract void onAfterExecute();

    public SuperAsyncTask(BaseActivity activity, int dialogId) {
        super();
        this.dialogId = dialogId;
        attach(activity);
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        mActivity.showDialog(dialogId); // go polymorphism!
    }    

    protected void onPostExecute(Result result) {
        super.onPostExecute(result);
        mResult = result;

        if (mActivity != null &&
                mActivity.mDialogMap.get((Integer) dialogId) != null
                && mActivity.mDialogMap.get((Integer) dialogId)) {
            postExecution();
        }
    };

    public void attach(BaseActivity activity) {
        this.mActivity = activity;
    }

    public void detach() {
        this.mActivity = null;
    }

    public synchronized boolean postExecution() {
        Boolean dialogExists = mActivity.mDialogMap.get((Integer) dialogId);
        if (dialogExists != null || dialogExists) {
            onAfterExecute();
            cleanUp();
    }

    public boolean cleanUp() {
        mActivity.removeDialog(dialogId);
        mActivity.mDialogMap.remove((Integer) dialogId);
        mActivity.cleanupTask();
        detach();
        return true;
    }
}
Олег Васкевич
источник
4

Кто-то из Google предоставил какое-то "официальное решение"?

Да.

Решение - это скорее предложение архитектуры приложения, а не просто код .

Они предложили 3 шаблона проектирования, которые позволяют приложению работать синхронно с сервером, независимо от состояния приложения (оно будет работать, даже если пользователь завершает приложение, пользователь меняет экран, приложение закрывается, любое другое возможное состояние, где операция фоновых данных может быть прервана, это покрывает это)

Это предложение объясняется в выступлении клиентских приложений REST для Android во время ввода-вывода Google 2010 Вирджилом Добянским. Это 1 час, но это очень стоит посмотреть.

Основой этого является абстрагирование сетевых операций от объекта, Serviceкоторый работает независимо от любого Activityв приложении. Если вы работаете с базами данных, использование ContentResolverи Cursorдаст вам готовый шаблон Observer, который будет удобен для обновления пользовательского интерфейса без какой-либо дополнительной логики, как только вы обновите свою локальную базу данных с извлеченными удаленными данными. Любой другой код после операции будет выполняться с помощью обратного вызова, переданного (для этого Serviceя использую ResultReceiverподкласс).

Во всяком случае, мое объяснение на самом деле довольно расплывчато, вы должны обязательно посмотреть речь.

Кристофер Франциско
источник
2

Хотя ответ Марка (CommonsWare) действительно работает для изменения ориентации, он терпит неудачу, если действие уничтожается напрямую (как в случае телефонного звонка).

Вы можете обрабатывать изменения ориентации И редкие уничтоженные события Activity, используя объект Application для ссылки на ASyncTask.

Там отличное объяснение проблемы и решения здесь :

Кредит полностью идет на Райана для выяснения этого.

Скотт Биггс
источник
1

Через 4 года Google решил проблему, просто вызвав setRetainInstance (true) в Activity onCreate. Это сохранит ваш экземпляр активности во время ротации устройства. У меня также есть простое решение для старых Android.

Singagirl
источник
1
Проблема, которую наблюдают люди, возникает из-за того, что Android уничтожает класс активности при ротации, расширении клавиатуры и других событиях, но асинхронная задача по-прежнему сохраняет ссылку на уничтоженный экземпляр и пытается использовать ее для обновления пользовательского интерфейса. Вы можете поручить Android не разрушать активность ни в манифесте, ни в прагматической форме. В этом случае ссылка на асинхронную задачу остается действительной, и никаких проблем не наблюдается. Поскольку ротация может потребовать дополнительной работы, например, перезагрузки просмотров и т. Д., Google не рекомендует сохранять активность. Итак, вы решаете.
Singagirl
Спасибо, я знал о ситуации, но не о setRetainInstance (). Что я не понимаю, так это ваше заявление о том, что Google использовал это для решения вопросов, заданных в вопросе. Можете ли вы связать источник информации? Спасибо.
JJ_
onRetainNonConfigurationInstance () Эта функция вызывается исключительно как оптимизация, и вы не должны полагаться на то, что она вызывается. <Из того же источника: developer.android.com/reference/android/app/…
Дхананджай М
0

Вы должны вызывать все действия активности, используя обработчик активности. Так что, если вы в какой-то теме, вы должны создать Runnable и опубликовать его с помощью обработчика Activitie. В противном случае ваше приложение будет зависать иногда с фатальным исключением.

xpepermint
источник
0

Это мое решение: https://github.com/Gotchamoh/Android-AsyncTask-ProgressDialog

В основном шаги:

  1. Я использую, onSaveInstanceStateчтобы сохранить задачу, если она еще обрабатывается.
  2. В onCreateI получить задание , если оно было сохранено.
  3. В onPauseI отбрасывать , ProgressDialogесли это показано.
  4. В onResumeI показать , ProgressDialogесли задача все еще обрабатывается.
Попался
источник