Практика создания приложений, отзывчивых к действиям пользователя, предполагает, что все тяжелые операции должны исполняться в отдельном потоке, сообщая тем или иным образом пользователю о своем прогрессе.
Android содержит массу способов для организации данного подхода, но одним из самых удобных можно признать использование AsyncTask и ProgressDialog.
Эта парочка превосходно решает задачу, но начинает приносить невыносимую боль, когда количество Activity с такой логикой переваливает за одну, что приводит к повторению управляющего кода, и еще большую боль, когда приложение должно поддерживать смену ориентации экрана.
AsyncTask
Для тех, кто не знаком с AsyncTask, поясню, что это специальный абстрактный класс, предоставляющий набор методов для реализации:
- onPreExecute для размещения инициализирующего кода (UI поток)
- doInBackground для размещения тяжелого кода, который будет выполняться в другом потоке
- onProgressUpdate для информирования о прогрессе (UI поток)
- onPostExecute для обработки результата, возвращенного doInBackground (UI поток)
- isCancelled чтобы узнать не отменил ли кто задачу
- publishProgress для перевода сообщения о прогрессе в UI поток с последующим вызовомonProgressUpdate
Суть проблемы
Использование упомянутых классов не представляет особого труда, достаточно пары сниппетов, чтобы код заработал как нужно и ProgressDialog начал свое информирование о ходе выполнения задачи. Но, как известно, дьявол кроется в деталях, поэтому стоит только поменять ориетацию экрана, как диалог пропадет, так же как и результат долгой, но невероятно ответственной операции.
Причина заключается в жизненном цикле Activity: смена ориентации экрана трактуется как смена конфигурации, что приводит к пересозданию Activity. Можно, конечно, отключить этот механизм, задав тег
android:configChanges="orientation"
у Activity и определив собственный код, который, при необходимости, произведет необходимые изменения. Но это будет необоснованное внедрение.Решением будет создание специального класса по управлению связкой Activity-AsyncTask-ProgressDialog, назовем его AsyncTaskManager.
Activity
Итак, в идеале наша Activity должна делать всего пять вещей (код из проекта примера):
- Создавать AsyncTaskManager в методе onCreate
mAsyncTaskManager = new AsyncTaskManager(this, this);
- Делегировать AsyncTaskManager восстановление задачи из состояния
mAsyncTaskManager.handleRetainedTask(getLastNonConfigurationInstance());
- Создавать конкретную задачу и отдавать ее в управление AsyncTaskManager’а
mAsyncTaskManager.setupTask(new Task(getResources()));
- Делегировать AsyncTaskManager сохранение задачи в состояние
return mAsyncTaskManager.retainTask();
- Обрабатывать асинхронное завершение задачи
public interface OnTaskCompleteListener {
void onTaskComplete(Task task);
}
В параметре метода будет передаваться задача, выполнение которой было завершено.
AsyncTaskManager
AsyncTaskManager должен отвечать за корректную работу всех компонентов, что сводится к списку следующих задач:
- Создавать диалог при инициализации
- При получении задачи в управление запускать ее
- Отображать состояние задачи в диалоге
- Отсоединяться от задачи после сохранения ее в состояние и присоединяться заново при восстановлении
- Отменять задачу при отмене диалога
- Закрывать диалог при завершении задачи
- Сообщать Activity о завершении либо отмене задачи
public interface IProgressTracker {
void onProgress(String message);
void onComplete();
}
Реализация:
@Override
public void onProgress(String message) {
if (!mProgressDialog.isShowing()) {
mProgressDialog.show();
}
mProgressDialog.setMessage(message);
}
@Override
public void onComplete() {
mProgressDialog.dismiss();
mAsyncTask.setProgressTracker(null);
mTaskCompleteListener.onTaskComplete(mAsyncTask);
mAsyncTask = null;
}
Присоединение к задаче:
mAsyncTask.setProgressTracker(this);
Отсоединение от задачи:
mAsyncTask.setProgressTracker(null);
mAsyncTask = null;
Отмена диалога:
@Override
public void onCancel(DialogInterface dialog) {
mAsyncTask.setProgressTracker(null);
mAsyncTask.cancel(true);
mTaskCompleteListener.onTaskComplete(mAsyncTask);
mAsyncTask = null;
}
AsyncTaskManager выполняет роль своеобразного ключа, который подключает и отключает работающую задачу к возможно пересозданному экземпляру Activity. Кроме того, он берет на себя и скрывает логику работы с ProgressDialog.
AsyncTask
Для задачи, помимо реализации основных методов, требуется реализация метода, который поможет соединять/разъединять ее с AsyncTaskManager’ом:
public void setProgressTracker(IProgressTracker progressTracker) {
mProgressTracker = progressTracker;
if (mProgressTracker != null) {
mProgressTracker.onProgress(mProgressMessage);
if (mResult != null) {
mProgressTracker.onComplete();
}
}
}
Как видно из приведенного кода, задача сохраняет вычисленный результат и последнее сообщение о прогрессе, и, в зависимости от состояния, вызывает тот или иной метод трекера (AsyncTaskManager’а).
Таким образом, даже если задача завершится до пересоздания Activity, та получит уведомление о завершении задачи.
Результат
Теперь можно безбоязненно крутить телефон в руках – все задачи будут обработаны корректно.
Использование такого менеджера существенно снижает количество кода в Activity и позволяет переиспользовать данную функциональность в проекте. Данный подход я разработал и успешно применил в своем недавнем приложении.
Ссылки
Архив с проектом примера
Описание AsyncTask (en)
Простая работа с потоками (en)
Диалоги в Android (en)
©HabraHabr.ru