1

Background

Some articles claim that Rx can replace AsyncTask and AsyncTaskLoader. Seeing that Rx usually makes code shorter, I tried to dive into various samples and ideas of how it works.

The problem

All of the samples and articles I've found, including Github repos, don't seem to really replace AsyncTask and AsyncTaskLoader well:

  1. Can't find how to cancel a task or multiple ones (including interrupting a thread). This is especially important for AsyncTask, which is usually used for RecyclerView and ListView. This can also be done on AsyncTaskLoader, but requires a bit more work than just call "cancel". For AsyncTask, it also provides publishing of a progress, and that's another thing I can't find an example of in Rx.

  2. Can't find how to avoid unsubscribing manually while avoiding memory leaks, which quite ruins the whole point of using Rx, as it's supposed to be shorter. In some samples, there are even more callbacks than normal code has.

What I've tried

Here are some links I've read about Rx:

The question

How can I use Rx as replacement for AsyncTask and AsyncTaskLoader?

For example, how would I replace this tiny AsyncTaskLoader sample code with Rx equivalent:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        supportLoaderManager.initLoader(1, Bundle.EMPTY, object : LoaderCallbacks<Int> {
            override fun onCreateLoader(id: Int, args: Bundle): Loader<Int> {
                Log.d("AppLog", "onCreateLoader")
                return MyLoader(this@MainActivity)
            }

            override fun onLoadFinished(loader: Loader<Int>, data: Int?) {
                Log.d("AppLog", "done:" + data!!)
            }

            override fun onLoaderReset(loader: Loader<Int>) {}
        })
    }

    private class MyLoader(context: Context) : AsyncTaskLoader<Int>(context) {

        override fun onStartLoading() {
            super.onStartLoading()
            forceLoad()
        }

        override fun loadInBackground(): Int {
            Log.d("AppLog", "loadInBackground")
            try {
                Thread.sleep(10000)
            } catch (e: InterruptedException) {
                e.printStackTrace()
            }
            return 123
        }
    }
}

For AsyncTask, usually we set it per ViewHolder, as a field there, and we cancel it upon onBindViewHolder, and when activity/fragment gets destroyed (go over all those that still pending or running).

Something like this (sample made as short as possible, of course it should be changed depending on needs) :

class AsyncTaskActivity : AppCompatActivity() {
    internal val mTasks = HashSet<AsyncTask<Void, Int, Void>>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_async_task)
        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
        recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
        recyclerView.adapter = object : Adapter<MyViewHolder>() {
            override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
                return MyViewHolder(LayoutInflater.from(this@AsyncTaskActivity).inflate(android.R.layout.simple_list_item_1, parent, false))
            }

            @SuppressLint("StaticFieldLeak")
            override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
                holder.tv.text = "loading..."
                if (holder.task != null) {
                    holder.task!!.cancel(true)
                    mTasks.remove(holder.task!!)
                }
                holder.task = object : AsyncTask<Void, Int, Void>() {
                    override fun doInBackground(vararg voids: Void): Void? {
                        for (i in 0..100)
                            try {
                                Thread.sleep(5)
                                publishProgress(i)
                            } catch (e: InterruptedException) {
                                e.printStackTrace()
                            }

                        return null
                    }

                    override fun onProgressUpdate(vararg values: Int?) {
                        super.onProgressUpdate(*values)
                        holder.tv.text = "progress:" + values[0]
                    }

                    override fun onPostExecute(aVoid: Void?) {
                        super.onPostExecute(aVoid)
                        mTasks.remove(holder.task)
                        holder.tv.text = "done:" + position
                        holder.task = null
                    }
                }.execute()
            }

            override fun getItemCount(): Int {
                return 1000
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        for (task in mTasks)
            task.cancel(true)
    }

    private class MyViewHolder(itemView: View) : ViewHolder(itemView) {
        internal var tv: TextView
        internal var task: AsyncTask<Void, Int, Void>? = null

        init {
            tv = itemView.findViewById(android.R.id.text1)
        }
    }
}
android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • About problem #1: In RxJava2, for every subscription, you get a `Disposable` interface, which has `dispose()` function. I believe that is equivalent to `AsyncTask`'s `cancel()` – Hendra Anggrian Oct 23 '17 at 09:32
  • About problem #2: I usually keep references of those `Disposable`s in collection, and `dispose()` every of them in `onDestroy()`, or `onDestroyView()` in Fragment. If this comment doesn't make sense then I clearly doesn't understand the premise of this one. – Hendra Anggrian Oct 23 '17 at 09:40
  • @HendraAnggrian Does "dispose" do more than just ignore the result? Does it allow to also interrupt the thread? Can you please post a sample code that's about as equivalent to what I did? AsyncTask also supports publishing a progress. Does Rx also support it? – android developer Oct 23 '17 at 10:20
  • My experience with RxJava only consist of 3 stream types: `Observable`, `Single`, and `Completable`. If you want to publish a progress, you'd want `Observable`, its `onNext()` is equivalent to `AsyncTask`'s `publishProgress()`. I would write a sample RxJava+RxAndroid code that mimics your `AsyncTask` code, but I see there is no `publishProgress()` in your code. Which in that case, `Single` or `Completable` may be preferable since they are simpler. Edit: I'm currently outside, will post code example in an hour or so. – Hendra Anggrian Oct 23 '17 at 10:31
  • I can easily add it. Just that I remembered it now, so I added about it too. I've now updated the code of AsyncTask sample, in the question. – android developer Oct 23 '17 at 10:44
  • dispose() does not ignore the result, it simply halts the emitter so that it won't emit more items, not value, not error, not even completion. – Hendra Anggrian Oct 23 '17 at 11:47
  • @HendraAnggrian Since it doesn't pass the result/s anymore, I consider it to ignore all that it has done to get it. The emitter can still try to emit items, no? And so they are ignored... Or am I wrong? In which place in the code does it have a chance to stop? Shouldn't it have a check "ifDisposed() return" or something like that? – android developer Oct 23 '17 at 12:23

2 Answers2

0

Let me tell you in advance that my experience in RxJava isn't as some would call it advance. I just know enough to replace AsyncTask using RxJava + RxAndroid. For example, let's convert your AsyncTask code to RxJava2 equivalent using Observable, which is arguably the most popular stream type.

Observable
    .create <Int> { emitter ->
        try { 
            for (i in 0..100) {
                Thread.sleep(5) 
                emitter.onNext(i) 
            }
        } catch (e: InterruptedException) {
            emitter.onError(e)
        }
        emitter.onComplete()
    ‎}
    ‎.subscribeOn(Schedulers.io())
    ‎.observeOn(AndroidSchedulers.mainThread())
    ‎.subscribe({ i ->
    ‎    holder.tv.text = "progress:" + i
     }, { e ->
     ‎   e.printStackTrace()
     ‎}, {
     ‎   holder.tv.text = "done:" + position
     ‎})

Now let's go through the code line-by-line:

  1. create lets you customize emitter behavior. Note that this isn't the only method to create the streams, there are also just, fromArray, fromList, defer, etc. We can discuss more of that if you are still interested.
  2. subscribeOn(Schedulers.io()) tells RxJava that #1 will be executed in I/O thread. If this is a background operation, Schedulers.newThread() also works (at least to my knowledge it does).
  3. observeOn(AndroidSchedulers.mainThread()) tells RxJava that #4 will be executed in Android's main thread. This is also where RxAndroid fills the gap since RxJava doesn't have any reference of Android.
  4. subscribe()
    • onNext(i : Int) - equivalent to publishProgress(). In newer RxJava, emitted value cannot be null.
    • onError(e : Throwable) - error handling that is long glorified by RxJava's programmers. In newer RxJava, onError() must be provided, doesn't matter if it's empty.
    • onComplete() - equivalent to onPostExecute().

Again, there might be a lot of misinformation with my answer. I expect someone with better understanding to correct it or provide better answer that this one.

Hendra Anggrian
  • 5,780
  • 13
  • 57
  • 97
  • Do I create it in the same place I created a new instance of AsyncTask? How can I make it stop its task? Call dispose() ? Can I also interrupt the thread this way? Also, writing the code you wrote, I have errors of this type "Expecting an element" for every end of code block you wrote. How come? – android developer Oct 23 '17 at 13:13
  • I would create a base activity where these Disposables are collected, and dispose them all onDestroy(), something like [this](https://github.com/WijayaPrinting/wp-android/blob/master/app/src/com/wijayaprinting/android/activity/WPActivity.kt). About that code snippet, I am unable to reproduce the error you have mentioned: https://imgur.com/a/ejzDt – Hendra Anggrian Oct 23 '17 at 16:18
  • It has this issue. Here: https://i.stack.imgur.com/ne9qq.png . I don't know what it wants exactly. In any case, how would you also handle AsyncTaskLoader? It's like AsyncTask, but is more bound to the Activity, so that if the configuration changes, the new Activity can still reach it, know its state and result, including to have a callback when it's done. I think the solution for this might be similar to what you did on you "rx-activity" library. – android developer Oct 24 '17 at 10:56
  • How can they expect an element? All 3 of them (onNext, onError and onComplete) have void return type. Try adding `Unit` as last line of each of them. – Hendra Anggrian Oct 24 '17 at 13:41
0

It seems there is no way to implement AsyncTask functionality with RxJava in shorter or more simple form. With RxJava you simplify task chaining, so if you have only one task AsyncTask can be better solution, but when you have several consecutive tasks to do - RxJava's interface is more convenient. So instead of, for example, program onCancelled for each task you should rather implement some common logic.

Anton Malyshev
  • 8,686
  • 2
  • 27
  • 45
  • I don't understand. Do you say that Rx shouldn't be used as replacement for either AsyncTaskLoader and AsyncTask? AsyncTaskLoader is usually used single-time per Activity (because it saves its state across Activity configuration change), while AsyncTask is usually created multiple times, as it's usually used in small tasks such as in RecyclerView and ListView. – android developer Oct 23 '17 at 14:04
  • I only suggested to use each approach when it's more handy. For several consecutive async actions (for example, load -> transform -> load another part -> join) `RxJava` code is more transparent. The central point here is consecutiveness. – Anton Malyshev Oct 23 '17 at 14:14
  • Is it possible though to use Rx in those cases? Of course the sample isn't the best use case, but it's just a sample. I wanted to write as shorter code as I can, to demonstrate how it can be done in the general way. – android developer Oct 23 '17 at 14:28
  • Yes, it's possible. You only need to manually code progress tracking, to do some cleanup after calling `dispose`, etc. – Anton Malyshev Oct 24 '17 at 11:51