Клас AsyncTask (застарілий)

У попередніх статтях був описаний загальний підхід, який застосовується зараз для запуску нового потоку в додатку та оновлення в ньому користувацького інтерфейсу. Розглянемо інший підхід, який представляє клас AsyncTask. Хоча застосування AsyncTask в сучасних додатках Android застаріло, тим не менш, оскільки він досі широко застосовується, також розглянемо його.

Щоб використовувати AsyncTask, нам потрібно:

  1. Створити клас, який успадковується від AsyncTask (як правило, для цього створюється внутрішній клас у activity або у фрагменті)
  2. Перевизначити один або кілька методів AsyncTask для виконання деякої роботи у фоновому режимі
  3. При необхідності створити об'єкт AsyncTask і викликати його метод execute(), щоб почати роботу

Отже, створимо найпростіший додаток із використанням AsyncTask. Визначимо наступну розмітку інтерфейсу у файлі activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="16dp"
    android:orientation="vertical">
 
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="22sp"
            android:id="@+id/clicksView"
            android:text="Clicks: 0"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/clicksBtn"
            android:text="Click" />
    </LinearLayout>
 
    <Button
        android:id="@+id/progressBtn"
        android:text="Запуск"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
 
    <TextView
        android:id="@+id/statusView"
        android:text="Статус"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <ProgressBar
        android:id="@+id/indicator"
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="100"
        android:progress="0" />
</LinearLayout>

Тут визначено кнопку для запуску фонового потоку, а також текстове поле і прогрессбар для індикації виконання завдання. Крім того, тут визначено додаткову кнопку, яка збільшує кількість кліків, і текстове поле, яке виводить число кліків.

Далі визначимо в класі MainActivity такий код:

import android.os.AsyncTask;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
 
public class MainActivity extends AppCompatActivity {
 
    int[] integers=null;
    int clicks = 0;
    ProgressBar indicatorBar;
    TextView statusView;
    TextView clicksView;
    Button progressBtn;
    Button clicksBtn;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        integers = new int[100];
        for(int i=0;i<100;i++) {
            integers[i] = i + 1;
        }
        indicatorBar = (ProgressBar) findViewById(R.id.indicator);
        statusView = findViewById(R.id.statusView);
        progressBtn = findViewById(R.id.progressBtn);
        progressBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
 
                new ProgressTask().execute();
            }
        });
 
        clicksView = findViewById(R.id.clicksView);
        clicksBtn = findViewById(R.id.clicksBtn);
        clicksBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
 
                clicks++;
                clicksView.setText("Clicks: " + clicks);
            }
        });
    }
 
    class ProgressTask extends AsyncTask<Void, Integer, Void> {
        @Override
        protected Void doInBackground(Void... unused) {
            for (int i = 0; i<integers.length;i++) {
 
                publishProgress(i);
                SystemClock.sleep(400);
            }
            return(null);
        }
        @Override
        protected void onProgressUpdate(Integer... items) {
            indicatorBar.setProgress(items[0]+1);
            statusView.setText("Статус: " + String.valueOf(items[0]+1));
        }
        @Override
        protected void onPostExecute(Void unused) {
            Toast.makeText(getApplicationContext(), "Задача завершена", Toast.LENGTH_SHORT)
                    .show();
        }
    }
}

Клас задачі ProgressTask визначений як внутрішній клас. Він успадковується не просто від AsyncTask, а від його типізованої версії AsyncTask<Void, Integer, Void>. Вона типізується трьома типами:

  1. Клас для зберігання інформації, яка потрібна для обробки задачі
  2. Тип об'єктів, які використовуються для індикації процесу виконання задачі
  3. Тип результату задачі

Ці типи можуть бути представлені різними класами. У даному випадку сутність задачі буде полягати в переборі масиву integers, що представляє набір елементів Integer. І тут нам не треба передавати в задачу жоден об'єкт, тому перший тип йде як Void.

Для індикації перебору використовуються цілі числа, які показують, який об'єкт з масиву ми зараз перебираємо. Тому в якості другого типу використовується Integer.

В якості третього типу знову використовується Void, оскільки в даному випадку не потрібно нічого повертати з задачі.

AsyncTask містить чотири методи, які можна перевизначити:

  1. Метод doInBackground(): виконується у фоновому потоці, повинен повертати певний результат
  2. Метод onPreExecute(): викликається з головного потоку перед запуском методу doInBackground()
  3. Метод onPostExecute(): виконується з головного потоку після завершення роботи методу doInBackground()
  4. Метод onProgressUpdate(): дозволяє сигналізувати користувачеві про виконання фонового потоку

Оскільки метод doInBackground() не приймає нічого і не повертає нічого, то в якості його параметра використовується Void... - масив Void, і в якості повернутого типу - також Void. Ці типи відповідають першому та третьому типам в AsyncTask<Void, Integer, Void>.

Метод doInBackground() перебирає масив і при кожній ітерації сповіщає систему за допомогою методу publishProgress(item). Оскільки в нашому випадку для індикації використовуються цілі числа, то параметр item має представляти ціле число.

Метод onProgressUpdate(Integer... items) отримує передане вище число і застосовує його для налаштування текстового поля та прогресбару.

Метод onPostExecute() виконується після завершення задачі і в якості параметра приймає об'єкт, що повертається з методу doInBackground() - тобто в даному випадку об'єкт типу Void. Щоб сигналізувати про закінчення роботи, тут виводиться на екран спливаюче повідомлення.

Запустимо додаток. Запустимо завдання, натиснувши на кнопку: При цьому поки виконується завдання, ми можемо паралельно натискати на другу кнопку і збільшувати кількість кліків, або виконувати якусь іншу роботу в застосунку.

AsyncTask і фрагменти

Під час використання AsyncTask слід враховувати такий момент. Оптимальнішим способом є робота AsyncTask із фрагментом, ніж безпосередньо з activity. Наприклад, якщо ми візьмемо попередній проєкт, запустимо застосунок і змінимо орієнтацію мобільного пристрою, то відбудеться перестворення activity. У разі зміни орієнтації пристрою потік AsyncTask продовжуватиме звертатися до старої activity, замість нової. Тому в цьому випадку краще використовувати фрагменти.

Отже, візьмемо проєкт з минулої теми і додамо в нього новий фрагмент, який назвемо ProgressFragment. Визначимо для нього новий файл розмітки інтерфейсу fragment_progress.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_progress"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="16dp"
    android:orientation="vertical">
    <Button
        android:id="@+id/progressBtn"
        android:text="Запуск"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
 
    <TextView
        android:id="@+id/statusView"
        android:text="Статус"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <ProgressBar
        android:id="@+id/indicator"
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="100"
        android:progress="0" />
</LinearLayout>

Сам клас фрагмента ProgressFragment змінимо таким чином:

package com.example.eugene.asyncapp;
 
import android.widget.Button;
import android.os.AsyncTask;
import android.os.Bundle;
import android.app.Fragment;
import android.os.SystemClock;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import android.view.View.OnClickListener;
 
public class ProgressFragment extends Fragment {
 
    int[] integers=null;
    ProgressBar indicatorBar;
    TextView statusView;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_progress, container, false);
        integers = new int[100];
        for(int i=0;i<100;i++) {
            integers[i] = i + 1;
        }
        indicatorBar = (ProgressBar) view.findViewById(R.id.indicator);
        statusView = (TextView) view.findViewById(R.id.statusView);
        Button btnFetch = (Button)view.findViewById(R.id.progressBtn);
        btnFetch.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
 
                new ProgressTask().execute();
            }
        });
        return view;
    }
 
    class ProgressTask extends AsyncTask<Void, Integer, Void> {
        @Override
        protected Void doInBackground(Void... unused) {
            for (int i = 0; i<integers.length;i++) {
 
                publishProgress(i);
                SystemClock.sleep(400);
            }
            return null;
        }
        @Override
        protected void onProgressUpdate(Integer... items) {
            indicatorBar.setProgress(items[0]+1);
            statusView.setText("Статус: " + String.valueOf(items[0]+1));
        }
        @Override
        protected void onPostExecute(Void unused) {
            Toast.makeText(getActivity(), "Задача завершена", Toast.LENGTH_SHORT)
                    .show();
        }
    }
}

Тут визначено всі ті дії, які були розглянуті в минулій темі і які раніше перебували в класі MainActivity. Особливо варто відзначити виклик setRetainInstance(true) у методі onCreate(), який дає змогу зберігати стан фрагмента незалежно від зміни орієнтації.

Тепер зв'яжемо фрагмент з activity. Для цього визначимо у файлі activity_main.xml такий код:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <fragment
        android:id="@+id/progressFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="com.example.eugene.asyncapp.ProgressFragment"/>
</LinearLayout>

А сам клас MainActivity скоротимо:

package com.example.eugene.asyncapp;
 
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
 
public class MainActivity extends AppCompatActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

Тепер якщо ми запустимо додаток, то незалежно від зміни орієнтації мого мобільного пристрою фонове завдання продовжуватиме свою роботу: