AsyncTask

Background

  |   6 min czytania

Wstęp

AsyncTask umożliwia wykonanie zadania w tle i publikowanie stanu pracy w interfejsie użytkownika bez konieczności manipulowania wątkami. Jest zaprojektowany jako klasa pomocnicza wokół klasy Thread i Handler. Wykorzystywany przede wszystkim do krótkich jednorazowych operacji powiązanych z bieżącym ekranem, które wymagają przetworzenia rezultatu na wątku głównym aplikacji. Dostarcza prosty w obsłudze interfejs co zwalnia programistę z obowiązku zajmowania się obsługą wątków (roboczego i głównego).

Implementacja

Aby stworzyć zadanie asynchroniczne należy rozszerzyć klasę AsyncTask oraz nadpisać przynajmniej metodę doInBackground odpowiedzialną za wykonywanie zadania na wątku roboczym. Pozostałe metody onPreExecute, onProgressUpdate, onPostExecute, onCancelled są wykonywane na głównym wątku dla różnych stanów zadania. Częstą praktyką jest implementacja klasy AsyncTask jako klasy wewnętrznej (inner class) w kontekście wywołania. Rozpoczęcie wykonywania zadania odbywa się przy pomocy metody execute.

//first of generic types are params, second progress and third is final result
private class CustomAsyncTask : AsyncTask<String, Int, String>() {

    override fun onPreExecute() {
        //prepare before running background job like show progress dialog
    }

    override fun doInBackground(vararg params: String): String {
        //some background job with passed params like URLs
        var total = ""
        for((counter, url) in params.withIndex()) {
            //download info from url
            val result = "result from : $url\n"
            total = total.plus(url)
            publishProgress(counter) //inform that some part of full request if completed
        }
        return total
    }

    override fun onProgressUpdate(vararg values: Int?) {
        //show some progress updates based on Int from publishProgress
    }

    override fun onPostExecute(result: String) {
        //do something on main thread when background job finished like update view
    }

    override fun onCancelled() {
        //stop and clear or show some message
    }
}

//to start just call
//CustomAsyncTask().execute("url1", "url2", "url3")

Ograniczenia

Implementacja klasy AsyncTask jako klasy wewnętrznej może powodować wycieki pamięci (leak memmory). Instancja takiej klasy przechowuje referencje do klasy zewnętrznej (np. Activity, Fragment) niezależnie od stanu cyklu życia obiektu klasy zewnętrznej co powstrzymuje Garbage Collector przed usunięciem referencji. Aby temu zapobiec należy stworzyć klasę jako statyczną wewnętrzną (static inner), zagnieżdżoną (nested) lub na poziomie top-level. W przypadku wymaganej referencji do Activity warto posłużyć się słabą referencją WeakReference lub przekazać obiekt Listener implementujący oczekiwane zachowanie interfejsu użytkownika.

class MainActivity : AppCompatActivity(), Listener {
    
    //reference to task allows to manage it's lifecycle
    private var asyncTask : SaferAsyncTask? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
		
        button.setOnClickListener {
            //AsyncTask is single shot, so create new task every time
            if(asyncTask == null || asyncTask?.status != AsyncTask.Status.RUNNING) {
                asyncTask = SaferAsyncTask(this)
                asyncTask?.execute("url1", "url2", "url3")
            }
        }
    }

    override fun onDestroy() {
        //stop the task when exit from Activity
        if(asyncTask?.status == AsyncTask.Status.RUNNING)
            asyncTask?.cancel(true)
        super.onDestroy()
    }

    override fun onStarting() {
        //create progress dialog
    }

    override fun onProgress(progress: Int) {
        //update progress dialog
    }

    override fun onFinished(result: String) {
        //close progress dialog
        //show the result
    }

    override fun onCancel() {
        //close progress dialog
        //show error message
    }

    private class SaferAsyncTask(listener : Listener) : AsyncTask<String, Int, String>() {

        private val reference = WeakReference<Listener>(listener)

        override fun onPreExecute() {
            reference.get()?.onStarting()
        }

        override fun doInBackground(vararg params: String): String {
            var total = ""
            for((counter, url) in params.withIndex()) {
                val result = "result from : $url\n"
                total = total.plus(result)
                publishProgress(counter) //inform that some part of full request if completed
            }
            return total
        }

        override fun onProgressUpdate(vararg values: Int?) {
            values[0]?.let { reference.get()?.onProgress(it) }
        }

        override fun onPostExecute(result: String) {
            reference.get()?.onFinished(result)
        }

        override fun onCancelled() {
            reference.get()?.onCancel()
        }
    }
}

private interface Listener {
    fun onStarting()
    fun onProgress(progress : Int)
    fun onFinished(result : String)
    fun onCancel()
}

Ponadto należy również zwrócić uwagę na fakt iż tak skonstruowane zadanie nie przetrwa zmiany konfiguracji co będzie skutkowało utratą aktualnego progresu. Aby temu zapobiec można umieścić obiekt AsyncTask w obiekcie Fragment z ustawionym setRetainInstance(true) lub w ViewModel. Co więcej, AsyncTask jest zadaniem jednorazowym i każde jego ponowne wywołanie wymaga nowej instancji w związku z czym warto zadbać o odpowiednią obsługę stanu zadania w zależności od cyklu życia. Ogranicza również komunikację z innymi zadaniami współbieżnymi wykonywanymi na innych wątkach.