Освой Android играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Класс AsyncTask

Создание новой асинхронной задачи
Простой пример для знакомства. Кот полез на крышу
Использование параметров
Используем второй параметр - промежуточные данные
Используем первый параметр - входящие данные
Используем третий параметр - возвращаемые данные
Метод get()
Метод cancel() - отменяем задачу
Текущее состояние задачи
Поворот экрана

В Android 11 класс AsyncTask объявлен устаревшим

Создание новой асинхронной задачи

Класс AsyncTask предлагает простой и удобный механизм для перемещения трудоёмких операций в фоновый поток. Благодаря ему вы получаете удобство синхронизации обработчиков событий с графическим потоком, что позволяет обновлять элементы пользовательского интерфейса для отчета о ходе выполнения задачи или для вывода результатов, когда задача завершена.

Следует помнить, что AsyncTask не является универсальным решением для всех случаев жизни. Его следует использовать для не слишком продолжительных операций - загрузка небольших изображений, файловые операции, операции с базой данных и т.д.

Напрямую с классом AsyncTask работать нельзя (абстрактный класс), вам нужно наследоваться от него (extends). Ваша реализация должна предусматривать классы для объектов, которые будут переданы в качестве параметров методу execute(), для переменных, что станут использоваться для оповещения о ходе выполнения, а также для переменных, где будет храниться результат. Формат такой записи следующий:


AsyncTask<[Input_Parameter Type], [Progress_Report Type], [Result Type]>

Если не нужно принимать параметры, обновлять информацию о ходе выполнения или выводить конечный результат, просто укажите тип Void во всех трёх случаях. В параметрах можно использовать только обобщённые типы (Generic), т.е. вместо int используйте Integer и т.п.

Соответственно, варианты могут быть самыми разными. Вот несколько из них


AsyncTask<Void, Void, Void>
AsyncTask<String, Integer, Integer>
AsyncTask<String, Void, Integer>
AsyncTask<String, Integer, String>

Для запоминания можно смотреть на схему.

Schema

Каркас реализации AsyncTask, в котором используются строковой параметр и два целочисленных значения, нужных для оповещения о выполнении работы и о конечном результате:


private class MyAsyncTask extends AsyncTask<String, Integer, Integer> {
    @Override
    protected Integer doInBackground(String... parameter) {
        int myProgress = 0;
        // [... Выполните задачу в фоновом режиме, обновите переменную myProgress...]
        publishProgress(myProgress);
        // [... Продолжение выполнения фоновой задачи ...]
        // Верните значение, ранее переданное в метод onPostExecute
        return result;
    }

    @Override
    protected void onProgressUpdate(Integer... progress) {
        // [... Обновите индикатор хода выполнения, уведомления или другой   
        // элемент пользовательского интерфейса ...]
    }
    
    @Override
    protected void onPostExecute(Integer... result) {
        // [... Сообщите о результате через обновление пользовательского 
        // интерфейса, диалоговое окно или уведомление ...]
    }
}

У AsyncTask есть несколько основных методов, которые нужно освоить в первую очередь. Обязательным является метод doInBackground(), остальные используются исходя из логики вашего приложения.

  • doInBackground() – основной метод, который выполняется в новом потоке. Не имеет доступа к UI. Именно в этом методе должен находиться код для тяжёлых задач. Принимает набор параметров тех типов, которые определены в реализации вашего класса. Этот метод выполняется в фоновом потоке, поэтому в нем не должно быть никакого взаимодействия с элементами пользовательского интерфейса. Размещайте здесь трудоёмкий код, используя метод publishProgress(), который позволит обработчику onProgressUpdate() передавать изменения в пользовательский интерфейс. Когда фоновая задача завершена, данный метод возвращает конечный результат для обработчика onPostExecute(), который сообщит о нём в поток пользовательского интерфейса.
  • onPreExecute() – выполняется перед doInBackground(). Имеет доступ к UI
  • onPostExecute() – выполняется после doInBackground() (может не вызываться, если AsyncTask был отменен). Имеет доступ к UI. Используйте его для обновления пользовательского интерфейса, как только ваша фоновая задача завершена. Данный обработчик при вызове синхронизируется с потоком GUI, поэтому внутри него вы можете безопасно изменять элементы пользовательского интерфейса.
  • onProgressUpdate(). Имеет доступ к UI. Переопределите этот обработчик для публикации промежуточных обновлений в пользовательский интерфейс. При вызове он синхронизируется с потоком GUI, поэтому в нём вы можете безопасно изменять элементы пользовательского интерфейса.
  • publishProgress() - можно вызвать в doInBackground() для показа промежуточных результатов в onProgressUpdate()
  • cancel() - отмена задачи
  • onCancelled() - Имеет доступ к UI. Задача была отменена. Имеются две перегруженные версии.

Вкратце о том, что значит имеет/не имеет доступ к UI. Все ваши кнопки, текстовые метки, ImageView (всё, что отображается на экране) являются частью пользовательского интерфейса - User Interface (UI). Ваша задача - не допустить, чтобы в методе doInBackground() было обращение к какому-нибудь элементу. Например, нельзя установить текст в TextView через метод setText() или поменять цвет шрифта в EditText. В примерах вы увидите, как нужно делать подобные вещи.

На заметку: Несмотря на то, что студия генерирует строки super.onPreExecute() и super.onPostExecute() для соответствующих методов, вы их можете удалить. В исходниках суперкласса методы ничего не делают (это просто заглушка), поэтому во многих примерах в интернете они опущены. Пусть вас это не пугает.

Простой пример для знакомства. Кот полез на крышу

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

Выведем на экран слово Полез на крышу в методе onPreExecute(), эмулируем тяжёлый код в методе doInBackground(), выведем на экран слово Залез в методе onPostExecute().

Создадим новый проект и добавим на экран кнопку, индикатор прогресса и текстовую метку:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical">

    <Button
        android:id="@+id/start_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onClick"
        android:text="Поехали" />

    <ProgressBar
        android:id="@+id/progressbar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/info_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="" />
</LinearLayout>

При щелчке кнопки должна запуститься тяжёлая задача. Это может быть загрузка файла из сети, обработка изображения, сохранение больших данных в файл или в базу данных. В нашем случае - кот полез на крышу. В TextView будем выводить текущую информацию. Компонент ProgressBar будет показывать, что приложение не зависло во время выполнения задачи.

Сначала напишем код, а потом будет объяснение к нему.


// Если этот код работает, его написал Александр Климов,
// а если нет, то не знаю, кто его писал.
package ru.alexanderklimov.asynctaskdemo;

import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import java.util.concurrent.TimeUnit;

public class MainActivity extends AppCompatActivity {

    private TextView mInfoTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mInfoTextView = findViewById(R.id.info_text);
    }

    public void onClick(View view) {
        CatTask catTask = new CatTask();
        catTask.execute();
    }

    class CatTask extends AsyncTask<Void, Void, Void> {

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            mInfoTextView.setText("Кот полез на крышу");
        }

        @Override
        protected Void doInBackground(Void... voids) {
            try{
                TimeUnit.SECONDS.sleep(5);
            }catch (InterruptedException e){
                e.printStackTrace();
            }

            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            mInfoTextView.setText("Кот залез на крышу");
        }
    }
}

Запустите проект и нажмите на кнопку. Сначала появится текст "Кот полез на крышу", который через 5 секунд сменится надписью "Кот залез на крышу". Индикаторе прогресса при этом будет постоянно крутиться.

У кота одна задача - залезть на крышу. Поэтому мы создали новую задачу CatTask, которая наследуется от AsyncTask. Для первого примера мы использовали Void, чтобы пока не связываться с параметрами.

В методе onPreExecute() мы устанавливаем начальный текст перед выполнением задачи.

В методе doInBackground() идёт имитация тяжёлой работы. Напоминаю, что здесь нельзя писать код, связанный с пользовательским интерфейсом. Ради интереса поместите в методе строчку:


mInfoTextView.setText("Лезу на крышу. Чуть-чуть осталось");

Студия будет ругаться и не позволит запустить приложение.

В методе onPostExecute() мы выводим сообщение, которое появится после выполнения задачи.

Обратите внимание, что если мы нажмём на кнопку, пока работает AsyncTask, то создастся и запустится новая задача поверх старой. Получается, что две задачи будут одновременно работать с экраном активности. Нужно избегать подобных ситуаций. Позже я покажу, как это сделать.

На данный момент для избежания конфликтов можно скрыть кнопку в методе onPreExecute() и показать её снова в onPostExecute():


// кнопку объявите самостоятельно
startButton.setVisibility(View.INVISIBLE); // прячем кнопку
startButton.setVisibility(View.VISIBLE);  // показываем кнопку

По такому же принципу можно показать и скрывать индикатор прогресса (сделайте самостоятельно).

Сама задача CatTask (объект AsyncTask) создаётся в UI-потоке. Также в UI-потоке вызывается метод объекта execute()

Каждый экземпляр класса AsyncTask может быть запущен всего один раз. Попытка повторного вызова метода execute() приведёт к выбросу исключения.

Кстати, коты прислали фотографию, которая объясняет, зачем они лезут на крышу. Интересно, откуда они узнали про эту статью?

Кот на крыше

Использование параметров

Для первого примера мы не использовали параметры. Наша задача была показать сообщения до и после выполнения задачи. Но часто нам необходимо знать промежуточные результаты выполняемой задачи. Например, мы хотим знать, что кот сейчас на втором этаже и ему осталось ещё двенадцать (бедный кот).

Когда мы создавали свой класс CatTask, то использовали угловые скобки, в которых необходимо указать три типа данных:

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

В первом примере мы указали <Void, Void, Void>. Эта запись означает, что мы не будем использовать параметры.

Во втором примере попробуем воспользоваться ими.

Используем второй параметр - промежуточные данные

Начнём со второго параметра, который отвечает за промежуточные данные. Итак, нам нужно знать текущий этаж, на котором находится кот. Если дом 14-этажный, то у нас должны быть значения от 1 до 14 типа Integer:


class CatTask extends AsyncTask<Void, Integer, Void>

Метод onPreExecute() оставляем без изменений, здесь мы просто выводим сообщение о начале штурма здания.

Переходим к методу doInBackground(). Мы ещё не используем входящие данные, поэтому пока здесь используется Void. В самом методе создаётся цикл от 0 до 14 и при каждом проходе цикла увеличивается счётчик counter на единицу.

В методе doInBackground() вызываются два метода. Первый метод getFloor() - наш. Здесь мы реализуем свою логику тяжёлой работы. Воспринимайте его как эмуляцию загрузки файлов или другую сложную работу. В нашем случае при покорении очередного этажа кот должен пометить его, нацарапать на стене неприличное слово из трёх букв типа МЯУ или МУР. В вашем приложении возможно это будет код обработки большой картинки, скачанной по сети. Пока у нас здесь просто пауза на одну секунду.

Второй метод publishProgress() - системный метод. Когда мы в методе doInBackground() вызываем метод publishProgress() и передаём туда данные, то срабатывает метод onProgressUpdate(), который получает эти данные. Тип принимаемых данных равен второму типу из угловых скобок, у нас это Integer. Метод onProgressUpdate() используется для вывода промежуточных результатов и имеет доступ к UI. Таким образом, метод publishProgress() является своеобразным мостиком для передачи данных из doInBackground() в onProgressUpdate(). Мы передаём значение счётчика, которое выводится в текстовой метке.


@Override
protected void onProgressUpdate(Integer... values) {
    super.onProgressUpdate(values);
    
    mInfoTextView.setText("Этаж: " + values[0]);
}

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

Для наглядности можно добавить на экран горизонтальный ProgressBar, который будет показывать в удобном виде график прохождения этажей.

AsyncTask

Полностью код будет следующим:


// Если этот код работает, его написал Александр Климов,
// а если нет, то не знаю, кто его писал.
package ru.alexanderklimov.asynctaskdemo;

import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import java.util.concurrent.TimeUnit;

public class MainActivity extends AppCompatActivity {

    private TextView mInfoTextView;
    private ProgressBar mProgressBar;
    private Button mStartButton;
    private ProgressBar mHorizontalProgressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mInfoTextView = findViewById(R.id.info_text);
        mStartButton = findViewById(R.id.start_button);
        mProgressBar = findViewById(R.id.progressbar);
        mHorizontalProgressBar = findViewById(R.id.horizontal_progress);
    }

    public void onClick(View view) {
        CatTask catTask = new CatTask();
        catTask.execute();
    }

    class CatTask extends AsyncTask<Void, Integer, Void> {

        @Override
        protected void onPreExecute() {
            super.onPreExecute();

            mInfoTextView.setText("Кот полез на крышу");
            mStartButton.setVisibility(View.INVISIBLE); // прячем кнопку
            mProgressBar.setVisibility(View.VISIBLE); // показываем индикатор прогресса
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);

            mInfoTextView.setText("Этаж: " + values[0]);
            mHorizontalProgressBar.setProgress(values[0]);
        }

        @Override
        protected Void doInBackground(Void... voids) {
            try {
                int counter = 0;

                for (int i = 0; i < 14; i++) {
                    getFloor(counter);
                    publishProgress(++counter);
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);

            mInfoTextView.setText("Кот залез на крышу");
            mStartButton.setVisibility(View.VISIBLE);  // показываем кнопку
            mProgressBar.setVisibility(View.INVISIBLE); // прячем бесконечный индикатор
            mHorizontalProgressBar.setProgress(0);
        }

        private void getFloor(int floor) throws InterruptedException {
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

Используем первый параметр - входящие данные

Теперь попробуем задействовать первый параметр, который отвечает за входящие данные. На практике это может быть список адресов, с которых надо загрузить картинки или что-то в этом роде.

Изменим объявление метода doInBackground():


protected Void doInBackground(String... urls)

У метода есть входные параметры типа String. Сразу поменяем первый параметр в классе CatTask:


class CatTask extends AsyncTask<String, Integer, Void> {
}

Теперь нужно передать в метод execute() список адресов файлов для загрузки. Метод doInBackground принимает эти данные и в цикле по одному загружает эти файлы. Дальше без изменений. После прохождения каждого этажа (загрузки каждого файла) вызывается метод publishProgress(), в который передаются данные.

В обработчике кнопки вызовем метод execute(), которому передадим набор строк, так как мы указали этот тип в угловых скобках на первом месте.


catTask.execute("cat1.jpg", "cat2.jgp", "cat3.jpg", "cat4.jpg");

В текстовой метке будет выводиться не только этаж, но и название картинки для каждого этажа. Для этого немного поправим второй параметр в CatTask.

Полностью код будет следующим:

Показать код (щелкните мышкой)

// Если этот код работает, его написал Александр Климов,
// а если нет, то не знаю, кто его писал.
package ru.alexanderklimov.asynctaskdemo;

import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import java.util.concurrent.TimeUnit;

public class MainActivity extends AppCompatActivity {

    private TextView mInfoTextView;
    private ProgressBar mProgressBar;
    private Button mStartButton;
    private ProgressBar mHorizontalProgressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mInfoTextView = findViewById(R.id.info_text);
        mStartButton = findViewById(R.id.start_button);
        mProgressBar = findViewById(R.id.progressbar);
        mHorizontalProgressBar = findViewById(R.id.horizontal_progress);
    }

    public void onClick(View view) {
        CatTask catTask = new CatTask();
        catTask.execute("cat1.jpg", "cat2.jpg", "cat3.jpg", "cat4.jpg");
    }

    class CatTask extends AsyncTask<String, String, Void> {

        @Override
        protected void onPreExecute() {
            super.onPreExecute();

            mInfoTextView.setText("Кот полез на крышу");
            mStartButton.setVisibility(View.INVISIBLE); // прячем кнопку
            mProgressBar.setVisibility(View.VISIBLE); // показываем индикатор прогресса
        }

        @Override
        protected void onProgressUpdate(String... values) {
            super.onProgressUpdate(values);

            mInfoTextView.setText("Этаж: " + values[0]);
            //mHorizontalProgressBar.setProgress(Integer.parseInt(values[0]));
        }

        @Override
        protected Void doInBackground(String... urls) {
            try {
                int counter = 0;

                for (String url : urls) {
                    // загружаем файл или лезем на другой этаж
                    getFloor(counter);
                    counter++;
                    // выводим промежуточные результаты
                    publishProgress(counter + " Картинка: " + url);
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);

            mInfoTextView.setText("Кот залез на крышу");
            mStartButton.setVisibility(View.VISIBLE);  // показываем кнопку
            mProgressBar.setVisibility(View.INVISIBLE); // прячем бесконечный индикатор
            mHorizontalProgressBar.setProgress(0);
        }

        private void getFloor(int floor) throws InterruptedException {
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

В нашем примере мы не загружаем картинки, поэтому строки с адресами изображений не задействованы. В других примерах вам будет попадаться такой код.

Ещё раз закрепим материал.

Метод execute() вызывается в основном потоке, чтобы начать выполнение задачи. В него можно передать набор данных определённого типа. Если что-то передаём, то этот тип будет указан первым в угловых скобках при создании наследника AsyncTask.

Методы onPreExecute() и onPostExecute() вызываются системой в начале и конце выполнения задачи.

Основной метод для тяжёлой работы - doInBackground(). Можно передать методу какие-то данные. В этом случае смотрим на execute().

Метод publishProgress() нужен в том случае, когда требуется обрабатывать промежуточные данные. Его нужно явно вызвать в методах doInBackground(), onPreExecute() или onPostExecute(). На вход передаём промежуточные данные определённого типа. Этот тип указан вторым в угловых скобках при описании AsyncTask.

Метод onProgressUpdate() получает на вход промежуточные результаты от метода publishProgress(). Так как метод onProgressUpdate() принимает на вход набор параметров, то при передаче одного значения от publishProgress нужно взять первый элемент массива.

Используем третий параметр - возвращаемые данные

Осталось рассмотреть вариант использования третьего параметра.

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

  • Он передаётся методу onPostExecute(), который вызывается по окончании задачи
  • Существует метод get(), способный возвращать нужный объект

Если мы собираемся что-то возвращать, то это надо указать в возвращаемом значении у метода doInBackground() и в входящем параметре для метода onPostExecute().

Начнём с метода doInBackground() и изменим его описание следующим образом:


@Override
protected Integer doInBackground(String... urls) {
    // здесь без изменений
    return 2012;
}

Как видите, наш метод теперь возвращает тип Integer. Пусть это будет число 2012 - напоминание про обман племени майя, которые обещали в том году конец света. Даже коты ждали его.

Кот и конец света

Студия подчеркнёт исправленный вариант, указывая несоответствие в параметрах у класса AsyncTask. Исправим:


class CatTask extends AsyncTask<String, String, Integer>

Теперь обратимся к методу onPostExecute(). Тип Integer должен стать входящим параметром. Поменяем объявление метода:


protected void onPostExecute(Integer result)

В нашем случае входящий параметр не несёт смысловой нагрузки, просто выведем значение в текстовом поле.

Полный код для самопроверки:

Показать код (щелкните мышкой)

// Если этот код работает, его написал Александр Климов,
// а если нет, то не знаю, кто его писал.
package ru.alexanderklimov.asynctaskdemo;

import ...

public class MainActivity extends AppCompatActivity {

    private TextView mInfoTextView;
    private ProgressBar mProgressBar;
    private Button mStartButton;
    private ProgressBar mHorizontalProgressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mInfoTextView = findViewById(R.id.info_text);
        mStartButton = findViewById(R.id.start_button);
        mProgressBar = findViewById(R.id.progressbar);
        mHorizontalProgressBar = findViewById(R.id.horizontal_progress);
    }

    public void onClick(View view) {
        CatTask catTask = new CatTask();
        catTask.execute("cat1.jpg", "cat2.jpg", "cat3.jpg", "cat4.jpg");
    }

    class CatTask extends AsyncTask<String, String, Integer> {

        @Override
        protected void onPreExecute() {
            super.onPreExecute();

            mInfoTextView.setText("Кот полез на крышу");
            mStartButton.setVisibility(View.INVISIBLE); // прячем кнопку
            mProgressBar.setVisibility(View.VISIBLE); // показываем индикатор прогресса
        }

        @Override
        protected void onProgressUpdate(String... values) {
            super.onProgressUpdate(values);

            mInfoTextView.setText("Этаж: " + values[0]);
            //mHorizontalProgressBar.setProgress(Integer.parseInt(values[0]));
        }

        @Override
        protected Integer doInBackground(String... urls) {
            try {
                int counter = 0;

                for (String url : urls) {
                    // загружаем файл или лезем на другой этаж
                    getFloor(counter);
                    counter++;
                    // выводим промежуточные результаты
                    publishProgress(counter + " Картинка: " + url);
                }

                //TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            return 2012;
        }

        @Override
        protected void onPostExecute(Integer result) {
            super.onPostExecute(result);

            mInfoTextView.setText("Кот залез на крышу" + " Возвращаем результат: " + result);
            mStartButton.setVisibility(View.VISIBLE);  // показываем кнопку
            mProgressBar.setVisibility(View.INVISIBLE); // прячем бесконечный индикатор
            mHorizontalProgressBar.setProgress(0);
        }

        private void getFloor(int floor) throws InterruptedException {
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

Метод get()

Выше уже упоминалось, что метод get() возвращает значение задачи. Тут следует рассмотреть два момента. Первый момент - вызовем метод после выполнения задачи. Второй момент - попробуем получить значение во время выполнения задачи.

Начнём с первого варианта. Добавим на экран активности ещё одну кнопку buttonResult с надписью "Получить результат":


<Button
    android:id="@+id/buttonResult"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="onResultButtonClick"
    android:text="Получить результат" />

Добавим код для обработчика кнопки:


// Объявим уже в классе и переделайте код у первой кнопки
private CatTask mCatTask;

public void onResultButtonClick(View view) {
    if (mCatTask == null)
        return;
    int result = -1;
    try {
        result = mCatTask.get();
        Toast.makeText(this, "Полученный результат: " + result, Toast.LENGTH_LONG)
                .show();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}

Запустите приложение, далее запустите задачу нажатием первой кнопки, дождитесь окончания задачи и нажмите вторую кнопку. Во всплывающем сообщение вы увидите то же число "2012", который возвращает метод onPostExecute().

А что случится, если задача находится в стадии выполнения, а мы вызовем метод get()? Давайте проверим.

Запустим снова приложение, нажмём на первую кнопку для запуска задачи и, не дожидаясь окончания задачи, нажмите на вторую кнопку.

Создаётся ощущение, что программа остановилась. ProgressBar перестанет обновляться, текстовые сообщения также не выводятся в TextView. Но после окончания задачи появится текст и вернётся результат. Что же произошло?. Метод get() блокирует основной UI-поток и ждёт завершения AsyncTask. Это продолжается до тех пор, пока не выполнится код в методе doInBackground(). После этого метод get() возвращает результат и освобождает поток.

Таким образом метод блокирует поток, в котором он выполняется, и не отпустит, пока не получит какой-то результат или не выскочит исключение.

Тайм-аут

Есть ещё реализация метода с тайм-аутом. В этом случае метод get() будет ждать указанное время, а потом сгенерирует Exception. Если же задача уже была завершена, то метод выполнится сразу и ждать ничего не будет.

Перепишем код для второй кнопки:


public void onResultButtonClick(View view) {
    if (mCatTask == null)
        return;
    int result = -1;
    try {
        result = mCatTask.get(1, TimeUnit.SECONDS);
        Toast.makeText(this, "Полученный результат: " + result,
                Toast.LENGTH_LONG).show();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    } catch (TimeoutException e) {
        Log.e("AsyncTaskDemo", "get timeout, result = " + result);
        e.printStackTrace();
    }
}

Смотрим, что получилось. Приложение снова подвисает, но через секунду оживает и продолжает работу. Метод get() ждёт одну секунду, и если не получает результат, то генерирует исключение TimeoutException. Мы обрабатываем исключение и выводим в лог соответствующее сообщение. А приложение продолжит выполнять задачу и после успешной обработки метода onPostExecute() выводит результат.

Метод cancel() - отменяем задачу

Метод cancel() позволяет указать на отмену уже выполняющейся задачи. У метода один boolean-параметр, который указывает, может ли система прервать выполнение потока.

Кроме того, в методе doInBackground() можно проверять метод isCancelled(). Как только мы выполним метод cancel(), метод isCancelled() будет возвращать true и мы должны завершить метод doInBackground(). Таким образом, метод cancel() служит своеобразной меткой, что задачу нужно отменить. А метод isCancelled() будет считывать результат предыдущего метода и позволет выполнит код для завершения задачи.

Вернёмся к нашей истории с упрямым котом, который лезет на крышу. Оставим на экране две кнопки для запуска и отмены задачи. Саму задачу немного упростим, убрав отвлекающий код (используем один из первых вариантов примера в начале статьи).


// Если этот код работает, его написал Александр Климов,
// а если нет, то не знаю, кто его писал.
package ru.alexanderklimov.asynctaskdemo;

import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;

import java.util.concurrent.TimeUnit;

public class MainActivity extends AppCompatActivity {

    private TextView mInfoTextView;
    //    private ProgressBar mProgressBar;
    private Button mStartButton;
    private ProgressBar mHorizontalProgressBar;

    private CatTask mCatTask;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mInfoTextView = findViewById(R.id.textViewInfo);

        mStartButton = findViewById(R.id.buttonStart);
        mHorizontalProgressBar = findViewById(R.id.progressBar2);
    }

    public void onClick(View view) {
        mCatTask = new CatTask();
        mCatTask.execute();
    }

    public void onCancelButtonClick(View v) {
        mCatTask.cancel(true);
    }

    class CatTask extends AsyncTask<Void, Integer, Void> {

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            mInfoTextView.setText("Полез на крышу");
            mStartButton.setVisibility(View.INVISIBLE);
        }

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

            try {
                int counter = 0;

                for (int i = 0; i < 14; i++) {
                    getFloor(counter);
                    publishProgress(++counter);
                    if (isCancelled())
                        return null;
                }
                TimeUnit.SECONDS.sleep(1);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            super.onPostExecute(result);
            mInfoTextView.setText("Залез");
            mStartButton.setVisibility(View.VISIBLE);
            mHorizontalProgressBar.setProgress(0);
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);

            mInfoTextView.setText("Этаж: " + values[0]);
            mHorizontalProgressBar.setProgress(values[0]);
        }

        @Override
        protected void onCancelled() {
            super.onCancelled();
            mInfoTextView.setText("Передумал лезть на крышу");
            mStartButton.setVisibility(View.VISIBLE);
            mHorizontalProgressBar.setProgress(0);
        }

        private void getFloor(int floor) throws InterruptedException {
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

Когда мы нажимаем на кнопку "Отменить операцию", то в методе cancel() используем параметр, равный true. В методе doInBackground() при работе цикла идёт проверка отмены (метод isCancelled()). Если приложение видит, что пользователь выбрал отмену задачи, то вместо метода onPostExecute() вызывается метод onCancelled(), в котором и прописываем свою логику кода.

В Android 4.0 появился ещё один метод onCancelled(Void result), способный принимать результат от метода doInBackground().


@Override
protected void onCancelled(Void result) {
    Log.d("AsyncTask", "onCancelled(Void) start");
    super.onCancelled(result);
    Log.d("AsyncTask", "onCancelled(Void) finish");
}

Текущее состояние задачи

Мы можем определить, в каком состоянии сейчас находится задача. Существует три состояния:

  • PENDING – задача не запущена
  • RUNNING – задача выполняется
  • FINISHED – задача успешно завершена. Метод onPostExecute() отработал

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


Toast.makeText(this, mCatTask.getStatus().toString(), Toast.LENGTH_SHORT).show();

Вкратце, когда вы создаёте задачу (new CatTask()), то состояние PENDING. Когда вы запускаете задачу (execute()) и задача работает, то состояние RUNNING. Когда задача завершится, то состояние FINISHED. Если вы отменили задачу, то состояние по-прежнему будет RUNNING, поэтому используйте проверку через метод isCancelled() для более точного определения состояния:


if (mCatTask.isCancelled())
    Toast.makeText(this, "Отмена задачи", Toast.LENGTH_SHORT).show();
else
    Toast.makeText(this, mCatTask.getStatus().toString(), Toast.LENGTH_SHORT).show();

В моих опытах после отмены сначала показывало состояние RUNNING, а после повторной проверки уже показывало FINISHED, возможно состояние не сразу устанавливается.

Поворот экрана

Как вы знаете, при повороте экрана активность пересоздаётся. А что происходит с задачей? При повороте старые объекты будут потеряны, в том числе и ссылка на нашу задачу. Фактически получается, что при повороте создаётся ещё одна задача, которая начинает выполняться с самого начала, при этом где-то выполняется и старая задача.

Ситуация не совсем приятная. Для ознакомления с решениями этой задачи рекомендую почитать статью Урок 91. AsyncTask. Поворот экрана, а заодно и другие материалы с сайта, связанные с AsyncTask.

Запустить несколько задач одновременно

AsyncTask в старых устройствах не поддерживал параллельное выполнение. Позже стало возможным запускать до пяти задач. Оставляю на память.


if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    TestAsyncTask firstAsync = new TestAsyncTask();
    firstAsync.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
else{
    TestAsyncTask firstAsync = new TestAsyncTask();
    firstAsync.execute();
}

Расширенный пример. Подготовим макет экрана из пяти индикаторов прогресса.


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/buttonStart"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Start"/>

    <ProgressBar
        android:id="@+id/progressBar1"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="100"
        android:progress="0"/>

    <ProgressBar
        android:id="@+id/progressBar2"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="100"
        android:progress="0"/>

    <ProgressBar
        android:id="@+id/progressBar3"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="100"
        android:progress="0"/>

    <ProgressBar
        android:id="@+id/progressBar4"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="100"
        android:progress="0"/>

    <ProgressBar
        android:id="@+id/progressBar5"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="100"
        android:progress="0"/>

</LinearLayout>

Запускаем первый индикатор, и параллельно с ним четвёртый и пятый при помощи executeOnExecutor(). Второй и третий индикатор начнут действовать после окончания работы всех предыдущих задач.


package ru.alexanderklimov.asynctask;

import android.annotation.TargetApi;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;

public class MainActivity extends AppCompatActivity {

    private ProgressBar mProgressBar1, mProgressBar2, mProgressBar3, mProgressBar4, mProgressBar5;
    MyAsyncTask asyncTask1, asyncTask2, asyncTask3, asyncTask4, asyncTask5;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mProgressBar1 = findViewById(R.id.progressBar1);
        mProgressBar2 = findViewById(R.id.progressBar2);
        mProgressBar3 = findViewById(R.id.progressBar3);
        mProgressBar4 = findViewById(R.id.progressBar4);
        mProgressBar5 = findViewById(R.id.progressBar5);

        Button startButton = (Button) findViewById(R.id.buttonStart);
        if (startButton != null) {
            startButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    asyncTask1 = new MyAsyncTask(mProgressBar1);
                    asyncTask1.execute();
                    asyncTask2 = new MyAsyncTask(mProgressBar2);
                    asyncTask2.execute();
                    asyncTask3 = new MyAsyncTask(mProgressBar3);
                    asyncTask3.execute();
                    asyncTask4 = new MyAsyncTask(mProgressBar4);
                    startAsyncTaskInParallel(asyncTask4);
                    asyncTask5 = new MyAsyncTask(mProgressBar5);
                    startAsyncTaskInParallel(asyncTask5);
                }
            });
        }
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    private void startAsyncTaskInParallel(MyAsyncTask task) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
            task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        else
            task.execute();
    }

    public class MyAsyncTask extends AsyncTask<Void, Integer, Void> {

        private ProgressBar mProgressBar;

        public MyAsyncTask(ProgressBar target) {
            mProgressBar = target;
        }

        @Override
        protected Void doInBackground(Void... params) {
            for (int i = 0; i < 100; i++) {
                publishProgress(i);
                SystemClock.sleep(100);
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            mProgressBar.setProgress(values[0]);
        }
    }
}

Примеры

Используем AsyncTask для загрузки изображений из сети

Используем AsyncTask для загрузки текстового файла из сети

AsyncTask устарел, что теперь?

Реклама