In the wake of trends, or moving towards RxJava and LiveData



    In the yard in 2018. Increasingly, the words RxJava and LiveData. But if suddenly it so happens that old-fashioned solutions like the android-priority-jobqueue or AsyncTask library (yes, it also happens so) rule in your application, then this article is for you. I share these approaches based on their philosophy. The first one assumes some dependence on the performance of the work on the display, the second one is the task execution in which the View listens to it and it is not interrupted depending on the life cycle events (for example, when the screen is rotated). Under the cut, I propose to consider the migration to a bunch of RxJava and LiveData for both approaches.

    For illustration, I will use the small class Work, which is some long-term operation that needs to be brought into the background thread. In the test application, I placed the spinning ProgressBar on the screen to see when the work will be performed in the main thread.

    classWork{
        fundoWork() = try {
            for (i in0 until 10) {
                Thread.sleep(500)
            }
            "work is done"
        } catch (e: InterruptedException) {
            "work is cancelled"
        }
    }
    

    AsyncTask


    With this approach, each task creates its own AsyncTask, which is canceled in onPause () or onStop (). This is done to ensure that the activation context does not leak. To show what is meant by this, I sketched out a small example.

    To begin with, we slightly modify the standard AsyncTask so that it can be canceled and return an error from it:

    classAsyncTaskCancellable<Params, Result>(
            privateval job: Job<Params, Result>,
            privatevar callback: AsyncTaskCallback<Result>?)
        : AsyncTask<Params, Void, AsyncTaskCancellable.ResultContainer<Result>>(), WorkManager.Cancellable {
        interfaceJob<Params, Result> {
            funexecute(params: Array<outParams>): Result
        }
        overridefundoInBackground(vararg params: Params): AsyncTaskCancellable.ResultContainer<Result> {
            returntry {
                ResultContainer(job.execute(params))
            } catch (throwable: Throwable) {
                ResultContainer(throwable)
            }
        }
        overridefunonPostExecute(result: AsyncTaskCancellable.ResultContainer<Result>) {
            super.onPostExecute(result)
            if (result.error != null) {
                callback?.onError(result.error!!)
            } else {
                callback?.onDone(result.result!!)
            }
        }
        overridefuncancel() {
            cancel(true)
            callback = null
        }
        classResultContainer<T> {
            var error: Throwable? = nullvar result: T? = nullconstructor(result: T) {
                this.result = result
            }
            constructor(error: Throwable) {
                this.error = error
            }
        }
    }
    

    Add the start of the execution of work in the manager:

    classWorkManager{
        fundoWorkInAsyncTask(asyncTaskCallback: AsyncTaskCallback<String>): Cancellable {
            return AsyncTaskCancellable(object : AsyncTaskCancellable.Job<Void, String> {
                overridefunexecute(params: Array<outVoid>) = Work().doWork()
            }, asyncTaskCallback).apply {
                execute()
            }
        }
    }
    

    We start the task, previously canceling the current one, if any:

    classMainActivity : AppCompatActivity() {
        ...
            loadWithAsyncTask.setOnClickListener {
                asyncTaskCancellable?.cancel()
                asyncTaskCancellable = workManager.doWorkInAsyncTask(object : AsyncTaskCallback<String> {
                    overridefunonDone(result: String) {
                        onSuccess(result)
                    }
                    overridefunonError(throwable: Throwable) {
                        this@MainActivity.onError(throwable)
                    }
                })
            }
        ...
    }
    

    Do not forget to cancel it in onPause ():

    overridefunonPause() {
        asyncTaskCancellable?.cancel()
        super.onPause()
    }
    

    This is where AsyncTask stops working and the callback is reset to clear the link to MainActivity. This approach is applicable when you need to perform a quick and minor task, the result of which is not terrible to lose (for example, when you flip the screen, when the activation is recreated).

    On RxJava, a similar implementation will not differ much.

    We also create Observable, which will be executed on Schedulers.computation () , and return it for further subscription.

    classWorkManager{
        ...
        fundoWorkInRxJava(): Observable<String> {
            return Observable.fromCallable {
                Work().doWork()
            }.subscribeOn(Schedulers.computation())
        }
        ...
    }
    

    We send callbacks to the main thread and subscribe to work:

    classMainActivity : AppCompatActivity() {
        ...
            loadWithRx.setOnClickListener { _ ->
            rxJavaSubscription?.dispose()
            rxJavaSubscription = workManager.doWorkInRxJava()
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({
                    onSuccess(it)
                }, {
                    onError(it)
                })
            }
        ...
    }
    

    Do not forget to clean up on yourself in onPause ():

    overridefunonPause() {
        rxJavaSubscription?.dispose()
        super.onPause()
    }
    

    In general, the implementation with RxJava can be a little added using the RxBinding library. It provides reactive strapping for Android components. In particular, in this case, you can use RxView.clicks () to get Observable, which allows you to listen to the button press:

    classMainActivity : AppCompatActivity() {
        ...
            rxJavaSubscription = RxView.clicks(loadWithRx)
                .concatMap {
                    workManager.doWorkInRxJava()
                        .observeOn(AndroidSchedulers.mainThread())
                        .doOnNext { result ->
                            onSuccess(result)
                        }
                        .onErrorReturn { error ->
                            onError(error)
                            ""
                        }
                    }
                .subscribe()
        ...
    }
    

    Error handling occurs in the onErrorReturn operator in order not to terminate the flow of button click events. Thus, if an error occurs while doing work, then it will not reach the final subscribe, and the clicks will continue to be processed.
    When implementing this approach, it must be remembered that the storage of Disposable, which returns subscribe (), in statics should be approached with caution. Until the dispose () method is called, it can store implicit references to your subscribers, which can lead to memory leaks.
    You also need to be careful with error handling in order not to accidentally finish the original stream.

    android-priority-jobqueue


    Here we have a certain manager who manages operations, and the display subscribes to his current status. The role of the layer between the manager and the UI is great for LiveData, which is tied to the life cycle. For the direct execution of the work in this example, I suggest using RxJava, which allows you to easily transfer the execution of the code to the background thread.

    We will also need the auxiliary wrapper class Resource, which will contain information about the status of the operation, an error and the result.

    classResource<T> privateconstructor(val status: Status,
                                                             valdata: T?,
                                                             val error: Throwable?) {
        constructor(data: T) : this(Status.SUCCESS, data, null)
        constructor(error: Throwable) : this(Status.ERROR, null, error)
        constructor() : this(Status.LOADING, null, null)
        enumclassStatus{
            SUCCESS, ERROR, LOADING
        }
    }
    

    Now we are ready to write the WorkViewModel class, which will contain the LiveData instance and notify it of changes in the work status using the Resource. In the example, I cheated a little and just made WorkViewModel a singleton. I use RxJava in statics, but I will subscribe to it through LiveData, so there will be no leaks.

    classWorkViewModelprivateconstructor() {
        companionobject {
            val instance = WorkViewModel()
        }
        privateval liveData: MutableLiveData<Resource<String>> = MutableLiveData()
        privatevar workSubscription: Disposable? = nullfunstartWork(work: Work) {
            liveData.value = Resource()
            workSubscription?.dispose()
            workSubscription = Observable.fromCallable {
                work.doWork()
            }
                .subscribeOn(Schedulers.computation())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({ liveData.value = Resource(it) },
                                { liveData.value = Resource(it) })
        }
        fungetWork(): LiveData<Resource<String>> = liveData
    }
    

    We supplement WorkManager by launching work to maintain uniformity, i.e. so that work always starts through this manager:

    classWorkManager{
        ...
        fundoOnLiveData() {
        WorkViewModel.instance.startWork(Work())
        }
        ...
    }
    

    And add interaction with all of this in MainActivity. From WorkViewModel, we get the status of the current work, while the display is alive, and we launch the new work by clicking on the button:

    classMainActivity : AppCompatActivity() {
        ...
        WorkViewModel.instance.getWork().observe(this, Observer {
            when {
                it?.status == Resource.Status.SUCCESS ->  onSuccess(it.data!!)
                it?.status == Resource.Status.ERROR -> onError(it.error!!)
                it?.status == Resource.Status.LOADING -> loadWithLiveData.isEnabled = false
            }
        })
        loadWithLiveData.setOnClickListener {
            workManager.doOnLiveData()
        }
        ...
    }
    

    In much the same way, this can be implemented using the Subject from RxJava. However, in my opinion, LiveData copes better with handling the life cycle, because it was originally sharpened for it, whereas with the Subject you can run into a lot of problems with stopping the flow and error handling. I think that the symbiosis of RxJava and LiveData is the most viable: the first receives and processes the data streams and notifies about the changes the second, to which you can already subscribe with an eye to the life cycle.

    Thus, we considered the transition from archaic libraries to more modern for the two most frequently encountered methods of performing work in the background thread. For a one-time minor operation, naked RxJava is perfect because it allows you to work very flexibly with the data and control the streams on which this should occur. At the same time, if a more subtle interaction with the life cycle is required, it is better to use LiveData, which was originally designed to solve this problem.

    The full version of the source code can be found here: GitHub

    I will be glad to answer your questions in the comments!

    Also popular now: