Zdarzenie asynchroniczne AsyncTask

Czasami dochodzi do sytuacji, kiedy musimy wykonać pewną operację zajmującą więcej czasu. Można oczywiście wykonać to "zwyczajnie" w kodzie, lecz użytkownik może odnieść wtedy wrażenie, że aplikacja się zawiesiła. Jest to oczywiście zdarzenie bardzo niepożądane. Na szczęście z pomocą przychodzi AsyncTask.

AsyncTask jest definiowany przez 3 typy generczne(z tego co widzę aktuanie nie mamy jeszcze artykułu o typach generycznych, ale postaram się w najbliższym czasie takowy dodać i tutaj podlinkować): Parametry, Progres i Wynik. Jest wykonywany w 4 krokach:

  • onPreExecute
  • doInBackgroud
  • onProgressUpdate
  • onPostExecute

Aby wykorzystać AsyncTask należy dziedziczyć (extends; więcej o dziedziczeniu tutaj i tutaj) po jej klasie.

import android.os.AsyncTask;

public class Obliczenia extends AsyncTask<Void, Void, Void>{

	@Override
	protected Void doInBackground(Void... arg0) {
		// TODO Auto-generated method stub
		return null;
	}

}

Spróbujmy napisać aplikację, która imituje zadanie wykonywane w tle.

screenshot

MainActivity.java

package com.example.asynctaskdemo;

import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.os.Bundle;
import android.view.View;

public class MainActivity extends Activity {

	public static final int PLEASE_WAIT_DIALOG = 1;

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

	public void buttonGo(View view) {

		new Obliczenia(this).execute();
	}

	@Override
	public Dialog onCreateDialog(int dialogId) {

		switch (dialogId) {
		case PLEASE_WAIT_DIALOG:
			ProgressDialog dialog = new ProgressDialog(this);
			dialog.setTitle("Obliczanie");
			dialog.setMessage("Proszę czekać....");
			dialog.setCancelable(true);
			return dialog;

		default:
			break;
		}

		return null;
	}

}

Warto zwrócić uwagę na obsługę przycisku. Nie używam tutaj metody findElementById() i nie przypisuję mu listenera, tylko korzystam z możliwości dodania do przycisku atrybutu onClick="nazwaMetody". Następnie wystarczy napisać publiczną metodę przyjmującą w parametrze View(referencję do przycisku który tą metodę wywołał).

Zauważcie, że nie tworzę sobie typu Obliczenia, tylko od razu po stworzeniu wywołuję na nim metodę run(). Jest to spowodowane m.in. tym, że AsyncTask możemy wykonać tylko raz. Aby wykonać zadanie ponownie należy stworzyć kolejną instancję.

Kolejną ciekawostką jest użycie metody onCreateDialog(int dialogID). Dlaczego tak, zamiast po prostu dialog.show()? Otóż dialog.show() może doprowadzić do wycieków pamięci w przypadku, gdy Activity w tle zostanie niespodziewanie zamknięte. W ten sposób całą odpowiedzialność za ewentualne wycieki pamięci przejmuje za nas Android, a my mamy jedną rzecz mniej o którą możemy się martwić. Niestety według dokumentacji od API 13 jest to metoda przedawniona i zaleca się teraz stosowanie kalsy DialogFragment z FragmentManager. Jeszcze się z tym nie zapoznałem. Jak zobaczę o co chodzi, to napiszę odpowiedni artykuł i podlinkuję tutaj.

Obliczenia:

package com.example.asynctaskdemo;

import android.app.Activity;
import android.os.AsyncTask;
import android.widget.Toast;

public class Obliczenia extends AsyncTask<Void, Void, Void> {

	Activity wywolujaceActivity;

	public Obliczenia(Activity wywolujaceActivity) {
		this.wywolujaceActivity = wywolujaceActivity;
	}

	@Override
	protected void onPreExecute() {
		wywolujaceActivity.showDialog(MainActivity.PLEASE_WAIT_DIALOG);
	}

	@Override
	protected Void doInBackground(Void... arg0) {
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return null;

	}

	@Override
	protected void onPostExecute(Void result) {
		wywolujaceActivity.removeDialog(MainActivity.PLEASE_WAIT_DIALOG);
		Toast.makeText(wywolujaceActivity, "Obliczono!", Toast.LENGTH_SHORT).show();
	}

}

Stworzyłem AsyncTask, który stosuje trzykrotnie typ Void - czyli nie przyjmuje żadnych parametrów, nie zwraca żadnych informacji o stanie i nie zwraca żadnego wyniku.

Dodałem swój konstruktor umożliwiający otrzymanie referencji do Activity która go wywołała, przez co mam możliwość wyświetlenia dialogu, oraz Toastu.

Same Obliczenia nic nie robią oprócz 5 sekund uśpienia, co dla celów demonstracyjnych dobrze się sprawdza.

activity_main.xml:

<RelativeLayout 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"
    tools:context=".MainActivity" >

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:onClick="buttonGo"
        android:text="Go!" />

</RelativeLayout>

Tutaj jedynką ciekawostką o której już wcześniej wspomniałem jest android:onClick="buttonGo", które umożliwia wygodniejsze i "czystsze" przechwycenie akcji guzika.

Możliwe, że w przyszłości rozwinę artykuł. Zależy głównie od zainteresowania tematem :)

Na razie tyle.

Po więcej informacji zapraszam do dokumentacji :)

Dyskusja i komentarze

Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.

Poniżej znajdziesz archiwalne wpisy z czasów, gdy strona była jeszcze hobbystycznym blogiem.

Mateusz

A jakbyśmy chcieli umieścić na jednym ekranie jakiś sekundnik, oraz np. TextView? Mógłbyś opisać problem?

Grzegorz

Tak jak kolega. Jak zmienić TextView w asynctask?

Fiddle

Obliczenia w klasie "Obliczenia" dopiszcie sobie pustą metode którą co sekundę wywołuje doInBackground następpnie tam gdzie tworzycie instancje "Obliczenia" nadpiszcie stworzoną metodę tak by edytowała co chcecie, będzie miała w tedy dostep do zmiennych lokalnych

Tolk

Czy jeden AsyncTask może wywoływać inny AsyncTask?

Marcin Kunert

Podejrzewam, że nie powinno być z tym problemu, ale jeszcze to sprawdzę.

Michał

"Zauważcie, że nie tworzę sobie typu Obliczenia, tylko od razu po stworzeniu wywołuję na nim metodę run()." Nie run() tylko execute()

gosciu

"z tego co widzę aktuanie nie mamy jeszcze artykułu o typach generycznych, ale postaram się w najbliższym czasie takowy dodać i tutaj podlinkować" Minęło 10miesięcy i nadal takowego artykułu nie ma...

Maciek

Czyli nie ma różnicy wyświetlania informacji o ładowaniu danych w sposób opisany wyżej czy w artykule "Ekran ładowania – Splash Screen"?