3. Класс AsyncTask для работы с фоновым потоком

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

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

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

Очень часто, работа с дополнительными потоками сводится к следующей формуле:

  1. подготовиться к открытию рабочего потока;

  2. выполнить задачу в рабочем потоке;

  3. по возможности показать промежуточный результат работы потока;

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

Как раз для таких случаев предусмотрен класс AsyncTask. Схема работы с этим классом уже знакома по опыту работы со стандартными классами в Android: создаем собственный класс, который расширяет класс AsyncTask и переопределяем нужные нам методы. Однако сигнатура и сценарий использования данного класса несколько отличаются от остальных.

Первое, что мы можем увидеть – AsyncTask является классом с 3(!) обобщенными типами. Какие типы указывать и на что они влияют – разберем позже, а пока что просто укажем Void в качестве обобщенных типов.

Класс AsyncTask является абстрактным и содержит абстрактный метод doInBackground(), который нам необходимо переопределить и реализовать в классе-наследнике. Чтобы «запустить» AsyncTask, необходимо создать объект созданного класса и вызвать метод execute().

MainActivity.java
public class MainActivity extends AppCompatActivity {
   
    class MyTask extends AsyncTask<Void, Void, Void> {

        @Override
        protected Void doInBackground(Void... voids) {
            return null;
        }
    }

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

        findViewById(R.id.button).setOnClickListener(v -> {
            MyTask myTask = new MyTask();
            myTask.execute();
        });
    }
}

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

  1. onPreExecute() – выполняется перед тем, как будет открыть новый поток, имеет доступ к UI и выполняется в основном потоке. Этот метод переопределяют, если необходимо выполнить какие-то действия перед началом работы нового потока;

  2. doInBackground() – основной метод AsyncTask`а, является обязательным для реализации. В этом методе указывается код, который будет выполнен в рабочем потоке. Код метода выполняется в рабочем потоке и не имеет доступ к UI;

  3. onProgressUpdate() – позволяет отобразить промежуточный результат выполнения рабочего потока. Код метода выполняется в UI потоке. Этот метод можно вызвать из метода doInBackground(). Метод не вызывается напрямую, а опосредовано через вызов метода publishProgress();

  4. onPostExecute() – выполняется после выполнения метода doInBackground(), код метода выполняется в UI потоке.

Таким образом, методы onPreExecute(), onProgressUpdate() и onPostExecute() выполняются в основном потоке, тогда как doInBackground() выполняется в рабочем потоке.

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

Теперь разберемся с обобщенными типами, которые мы указываем при создании класса-наследника AsyncTask. Мы указываем три типа данных:

  1. Тип входных данных. Когда вы вызываете метод execute() у объекта AsyncTask, вы можете передать данные для работы метода doInBackground(). Это может быть URL скачиваемого файла или объект SQL-запроса к базе данных или еще какие-либо данные, которые могут быть нужны для выполнения рабочего потока. Тип этих данных вы указываете как первый параметр при создании класса-наследника AsyncTask. Более подробно использование входных данных вы сможете увидеть на примере ниже, когда в качестве данных будет передана строка URL скачиваемого файла;

  2. Тип промежуточных данных. Если вам необходимо отобразить промежуточный результат выполнения рабочего потока, вы можете указать тип данных, которые будут переданы в метод onProgressUpdate(). Это может быть, например, процент выполнения задачи или промежуточный результат вычислений или что-либо еще. В примере ниже, мы будем передавать информацию о том, сколько процентов файла уже было загружено. Следовательно, в качестве типа промежуточных данных мы укажем тип Integer;

  3. Тип возвращаемых данных. В этом случае необходимо указать тип данных, которые вернет метод doInBackground(). Данные этого типа являются возвращаемым типом для метода doInBackground(), а также входными для метода onPostExecute(). Результат работы метода doInBackground() будет передан на вход метода onPostExecute().

Четыре правила использования класса AsyncTask.

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

  1. объект AsyncTask должен быть создан в UI-потоке;

  2. метод execute() должен быть вызван в UI-потоке;

  3. четыре вышеописанных метода не вызываются напрямую;

  4. объект AsyncTask может быть запущен только один раз (метод execute() должен быть вызван только один раз за время жизни объекта).

Пример использования класса AsyncTask.

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

MainActivity.java
public class MainActivity extends AppCompatActivity {

    private ProgressBar mProgressBar;

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

        mProgressBar = findViewById(R.id.progress);

        findViewById(R.id.button2).setOnClickListener(v -> {
            DownloadTask task = new DownloadTask();
            task.execute("https://download.sublimetext.com/Sublime%20Text%20Build%203207%20x64%20Setup.exe");
        });
    }

    private class DownloadTask extends AsyncTask<String, Integer, Boolean> {

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

            Toast.makeText(MainActivity.this,
                    "Началась загрузка файла", Toast.LENGTH_SHORT).show();
            mProgressBar.setProgress(0);
        }

        @Override
        protected Boolean doInBackground(String... params) {
            InputStream input = null;
            OutputStream output = null;
            HttpURLConnection connection = null;
            try {
                URL url = new URL(params[0]);
                connection = (HttpURLConnection) url.openConnection();
                connection.connect();
                if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
                    return false;
                }
                int fileLength = connection.getContentLength();
                input = connection.getInputStream();

                File path = MainActivity.this.getFilesDir();
                File file = new File(path, "/" + "file.rar");
                output = new FileOutputStream(file);

                byte data[] = new byte[4096];
                long total = 0;
                int count;
                while ((count = input.read(data)) != -1) {
                    if (isCancelled()) {
                        input.close();
                        return false;
                    }
                    total += count;
                    if (fileLength > 0) {
                        publishProgress((int) (total * 100 / fileLength));
                    }
                    output.write(data, 0, count);
                }
            } catch (Exception e) {
                return false;
            } finally {
                try {
                    if (output != null)
                        output.close();
                    if (input != null)
                        input.close();
                } catch (IOException ignored) {
                }
                if (connection != null)
                    connection.disconnect();
            }
            return true;
        }

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

        @Override
        protected void onPostExecute(Boolean isSuccessful) {
            String msg = "";
            if (isSuccessful) {
                msg = "Файл успешно загружен!";
            } else {
                msg = "Ошибка при скачивании файла!";
            }
            Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show();

            mProgressBar.setProgress(0);
        }
    }
}

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

Last updated