RxJava, What happened if I don't call dispose? - android

My Android app needs to support to upload a big file, but I do not want a user wait until the upload is completed.
Usually, when I use Observable, I call dispose() for it when the view is destroyed.
But in uploading case, I can not dispose it in any case until it finished.
So I was thinking to try to like this,
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
fun upload() {
val disposable = Observable.just(true).delay(20, TimeUnit.SECONDS).subscribe({
Log.d("=>", "Upload finished")
disposeUploader()
})
compositeDisposable.add(disposable)
}
fun disposeUploader() {
compositeDisposable.clear()
compositeDisposable.dispose()
}
But the problem is the upload() could be called multiple times, so the first uploader will dispose of all other processing calls.
What happened if I don't call dispose? or is there any way dispose of itself when it is completed?

The idea of disposing an Observable serves two purposes:
1) Avoid memory leaks - detach view references that might be held by ongoing request module.
2) Release resources - stop ongoing background task to free unneeded resources, when the user exit your activity for instance, a request/processing might not be relevant anymore, so no point to keep it running.
In your case you want your background task (uploading file) to resume, avoiding (2), while having your view detaching from it (1).
Your solution of disposing after some period of time miss the point as Observable will dispose itself at completion. moreover, you can't assume a specific duration for the upload (at most it is timeout value).
The solution with RxJava is multicasting your Observable using operator like publish and share, in your case:
val multicastedObservable = uploadObservable.publish()
.autoConnect()
.subscriber(//your view related subscriber);
This way, uploadObservable will start execute at first subscribe but will not stop when dispose is called, but rather will detach the reference to view.
Having said all that, it's worth noting that your upload scenario cannot be done reliably without foreground service in Android.

Related

How this change from runBlocking to sharedFlow work?

I would like to ask you why does it work?
Normally when I used collectLatest with flow my data wasn't collected on time and the return value was empty. I have to use async-await coroutines, but I have read it blocks main thread, so it is not efficient. I've made my research and find the solution using sharedflow.
Previously:
suspend fun getList: List<Items> {
CoroutineScope(Dispatchers.Main).launch {
async {
flow.collectLatest {
myItems = it
}
}.await()
}
return myItems
}
or without await-async and it returns emptyList
now:
suspend fun getList: List<Items> {
val sharedFlow = flow.conflate().shareIn(
coroutineScopeIO,
replay = 1,
started = SharingStarted.WhileSubscribed()
)
return sharedFlow.first()
}
conflate means:
Conflates flow emissions via conflated channel and runs collector in a separate coroutine. The effect of this is that emitter is never suspended due to a slow collector, but collector always gets the most recent value emitted.
I'm not sure I understand it clearly. When I conflate flow, I just create seperate coroutine to emit what will be inside my another function as in my example shareIn().first() and using this variablesharedFlow which is suspended so will give the same effect I made asnyc-await, but in that case I do not block main thread, but only my exact *parentCoroutine-or-suspendFunction?
SharingStarted.WhileSubscribed()
It just means to start emit when subcribed.
conflate() has nothing to do with why this is working. The separate coroutine it talks about is run under the hood and you don't need to think about it. It's just to make sure your flow never causes the upstream emitter to have to wait for a slow collector, and your collector skips values if they are coming faster than it can handle them. conflate() makes it safe to have a slow collector without a buffer.
In your first code block, you are launching a new coroutine in a new CoroutineScope, so it is not a child coroutine and will not be waited for before the function returns. (Incidentally, this new coroutine will only finish when the Flow completes, and most types of Flows never complete.)
In the second code block, you are calling first() on the Flow, which suspends and gets the next value emitted by the flow and then returns that value without waiting for the Flow to complete.
Some other notes:
You should never use async { /*...*/ }.await() where await() is called immediately on the Deferred, because it is just a more convoluted version of withContext(/*...*/) { /*...*/ }.
It's a code smell to create a CoroutineScope that you never assign to a property, because the point of creating a scope is so you can manage the scope, and you obviously aren't managing it if you have no reference to it to work with.
You said you are worried about blocking the main thread, but nothing in the code you showed looks suspicious of blocking the main thread. But it's possible your flow that you are basing this on has blocking code in it. By convention it shouldn't. If that flow blocks, you should use the flowOn(Dispatchers.IO) operator on it at the source so downstream users don't have to worry about it.
Although your code worked, it doesn't make sense to create a SharedFlow in a function and immediately collect from it. It's not being shared with anything! Your code could be simplified to this equivalent code:
suspend fun getList: List<Items> {
return flow.first()
}

Question about launchWhenX and repeatOnLifecycle

I have the following code and a few questions that need to be answered:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycleScope.launchWhenResumed {
delay(2000)
Log.d("LifeCycleAware", "launchWhenStarted: before calling")
val result = differentDispatcher()
Log.d("LifeCycleAware", "launchWhenStarted: after calling $result")
}
}
private suspend fun differentDispatcher(): Int =
withContext(Dispatchers.Default) {
for (i in 1..5) {
delay(2000)
Log.d("LifeCycleAware", "Inside different Dispatcher")
}
return#withContext 9
}
override fun onStart() {
super.onStart()
Log.d("LifeCycleAware", "onStart")
}
override fun onStop() {
super.onStop()
Log.d("LifeCycleAware", "onStop")
}
As far as I understand, the method (or any of them) launchWhenResumed, is called when the Lifecycle has reached the RESUMED state, and I also know that when I move the app to the background, the corrutine will stop, but it will not stop if it has child corrutines running in another Dispatcher, so far so good.
So in this code, we determine that if I, in the middle of the loop that is in the differentDispatcher method, send the app to second, it will continue to run but when it finishes, the parent corrutine launched with launchWhenResumed, will not resume until it takes this RESUMED state again.
My first doubt is... if when the corrutine is finished running, I go to the background and return to the foreground, why is it not launched again, if I have returned to the RESUMED state?
I also know about the existence of the repeatOnLifecycle method, where, if I pass the Lifecycle.State.RESUMED state as parameter, it is executed every time, moreover, I know that in this case if I go to the background, the execution of the corrutine is completely suspended and when I go back to the foreground it starts from the beginning, but, why when I run with launchWhenResumed and the corrutine finishes it does not start again, but with repeatOnLifecycle it does? What does it do differently internally?
I guess the answer is because when I switch from background to foreground, the onCreate method is not called again, and I've checked that:
override fun onResume() {
super.onResume()
lifecycleScope.launchWhenResumed {
delay(2000)
Log.d("LifeCycleAware", "launchWhenStarted: before calling")
val result = differentDispatcher()
Log.d("LifeCycleAware", "launchWhenStarted: after calling $result")
}
}
This way it does re-launch because the onResume method does call again when I switch from background to foreground but then.... what kind of magic does the repeatOnLifecycle method do?
The key to understanding launchWhenResumed is to break it down into the two parts that is actually is: a launch and a whenResumed block. Looking at the source code, you'll see it is actually exactly that:
public fun launchWhenResumed(
block: suspend CoroutineScope.() -> Unit
): Job = launch { // It does a regular launch
lifecycle.whenResumed(block) // Then passes your block to whenResumed
}
A launch is a one time operation - a launch done in onCreate() will only run exactly once for each call to onCreate(). This is also why calling launch in onResume() will launch every time you hit onResume.
A call to launch finishes in one of two ways: all of the calls within that block complete normally or the CoroutineScope is cancelled. For lifecycleScope, that cancellation happens when the Lifecycle reaches DESTROYED.
So in a regular launch, work starts immediately, runs until everything completes (or the scope is cancelled), and that's it. It never runs again or restarts at all.
Instead, the whenResumed is an example of Suspend Lifecycle-aware coroutines:
Even though the CoroutineScope provides a proper way to cancel long-running operations automatically, you might have other cases where you want to suspend execution of a code block unless the Lifecycle is in a certain state.
Any coroutine run inside these blocks is suspended if the Lifecycle isn't at least in the minimal desired state.
So what whenResumed does is just pause the coroutine code when you fall below the resumed state, essentially meaning that if your Lifecycle stops being RESUMED, instead of your val result = differentDispatcher() actually resuming execution immediately and your result being delivered back to your code, that result is simply waiting for your Lifecycle to again reach RESUMED.
So whenResumed doesn't have any 'restarting' functionality - just like other coroutine code, it just runs the code you've given it and then completes normally.
You're right that repeatOnLifecycle is a very different pattern. As per the Restartable Lifecycle-aware coroutines, repeatOnLifecycle doesn't have any of the 'pausing' behavior at all:
Even though the lifecycleScope provides a proper way to cancel long-running operations automatically when the Lifecycle is DESTROYED, you might have other cases where you want to start the execution of a code block when the Lifecycle is in a certain state, and cancel when it is in another state.
So in the repeatOnLifecycle call, every time your Lifecycle reaches RESUMED (or what Lifecycle.State you want), the block is ran. When you fall below that state, the whole block is completely cancelled (very similar to when your LifecycleOwner reaches DESTROYED - that level of cancelling the whole coroutine scope).
You'll not the dire warning at the end of the page that talks about both of these APIs:
Warning: Prefer collecting flows using the repeatOnLifecycle API instead of collecting inside the launchWhenX APIs. As the latter APIs suspend the coroutine instead of cancelling it when the Lifecycle is STOPPED, upstream flows are kept active in the background, potentially emitting new items and wasting resources.
The fact that your differentDispatcher code continues to run in the background, despite being inside a whenResumed block is considered a bad thing - if that code was doing more expensive operations, like checking for the user's location (keeping GPS on), it would continue to use up system resources and the user's battery the whole time, even when you aren't RESUMED.

How to handle thread execution in coroutines?

I have a video playing application. I have implemented Vertical Viewpager2 with fragment. The fragment contains fullscreen exoplayer where I load the video and have a button to open the profile of the user who have posted the video. I make all the API calls in viewmodel as
viewmodelScope.launch(Dispatchers.IO){
... //making api call and updating the livedata
...
}
I am also using
lifecycleScope.launch(Dispatchers.IO){
... //start caching the video
}
The Problem is :
When I scroll like 40 - 45+ items and launch any coroutine on Dispatchers.IO, the launch block is never executed hence the api calls are never made and app stops working.
Debugging results :
I tried to debug why the launch block is not executing with debug break points. In the LimitingDispatcher class, there is variable called inFlightTasks which is used to schedule the task if the parallelism limit is reached (which is 64 by default). So, my current task is added to ConcurrentLinkedQueue<Runnable>. I waited for an hour but after scheduling the task it never resumed the task again. I checked the queue for the tasks and at the time of testing it had around 150-170 tasks but none of them resumed.
Any idea how to solve this problem.
Note : I cannot remove the caching of video(which is am using in onStart() of the fragment).
EDIT
I tried with lifecycleScope.launch{ } and withContext(Dispatchers.IO){ }, but I am still getting the same issue.
In viewModelScope
viewModelScope.launch(Dispatchers.IO){
val response = repository.call()
when(repository){
is Resource.Sucess -> livedata.postValue(response.data)
is Resource.Error -> livedata.postValue(response.error)
}
}
In lifecycleScope
lifecycleScope.launch(Dispatcher.IO){
try{
startVideoCaching()
}catch(e : Exception){
e.printStackTrace()
}
}
And both the functions in launch are purely suspend functions.
Edit 2
I am adding the screen shots of my functions and the Dispatchers.IO's queue data.
Debug sscreenshot.
The functions named in the debug screenshot.
In the sendAnalytics I am using lifecycleScope only
cacheVideo() is called from onStart().
markVideoView() is called from onResume()
I tried with yield() but still no change in the behavior.
I am using all this methods in Fragment which is a part of ViewPager2 items.
If it reached the parallelism limit, and the other ones are put in the queue, the only
logical reason they're not resuming and executing is that those previously called
are not suspending/returning to free up the threads for the queued ones to run.
What you should do is make your functions cancellable (cooperatively cancellable using
isActive) and those coroutines that are fired for the items that are no longer visible
on the screen (scrolled off the screen) should be cancelled, since there is no point of
calculating something not visible on the screen. By cancelling those coroutines you
should always have enough free threads to run those coroutines for the items currently visible on the screen.
In case you don't want to cancel coroutines fired for the items not currently on the screen, you can call yield() inside your functions to deliberately yield thread to other coroutines that run on the same thread pool.

I want user start long complex backround task and control it's execution in Android. What to use?

I want user to start some complex background task and observe it's progres and being able to stop it. The process should not stop on screen rotation or application sent to background. The process should stop if user closes an application. The process should stop if user presses appropriate button.
What to use?
Just create a Thread? How would my Activity remember a thread after screen rotate or backgrounded? I can't serialize/parcelalize thread.
Background service? It is said deprecated here: https://developer.android.com/training/run-background-service/create-service
Immediate/Exact/Deferred from here https://developer.android.com/guide/background
But what namely? Diagram infers I should use Deffered, but the name contradicts, because I don't want to defer an execution.
Pls clarify.
We have MvvM architecture to tackle your problem, the ViewModel(A simple java/kt class) will still have its instance when the activity or fragment changes its configurations like(Device rotated, keyboard attached/detached etc). You can run your long task in viewmodel, and listen to the changes in your activity.
Like: ->
MyViewModel.kt
val longWork = MutableLiveData<String>()
fun performLongWork(){
// This is a viewmodel scope, which means,
// if the user has left the UI screen, any work that was started within
// this scope will finish and not report any result. But if you want to
// still continue the work even if the user left the screen,
// use CoroutineScope(IO).launch{}
viewModelScope.launch(Dispatchers.IO) {
//Some looooong task
delay(50000)
longWork.postValue("Completed :)")
}
}
Then in your activity or fragment
private fun observeWork() {
myViewModel.longWork.observe(viewLifecycleOwner, Observer {
it?.let { result ->
// Update the user that the work has completed
myTextView.setText(result)
}
})
}
If you want the work to be completed no matter what, even if the app is closed, or the device is restarted go for the below approach.
Just quoting from Android docs:
WorkManager is an API that makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app exits or the device restarts.
WorkManagerLink - This will also run immediately if you say it to.
WorkManager.getInstance(context)
.beginWith(workA)
.enqueue()
But if you add constraints like run when the device is charging, then this will obviously defer your execution.
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.build()
val myWorkRequest: WorkRequest =
OneTimeWorkRequestBuilder<MyWork>()
.setConstraints(constraints)
.build()
A very good video - Youtube Android Developers - Work Manager

Handling long running tasks with RxJava

I'm trying to migrate an AsyncTask that sends messages to the server, to use RxJava. Roughly, the task does the following:
1) Creates a message that will be sent (persists to the database)
2) Shows the message to the user (state 'sending')
3) Sends the message to the server (code snippet below)
4) Marks the message as sent or failed (persists to the database)
5) Updates the UI
I've created the required Rx chain which partially looks like this:
public Observable<Message> sendMessage(Message message) {
return mApiClient.sendMessage(message)
.doOnNext(sentMessage -> mDatabase.synchroniseMessage(sentMessage))
.doOnError(e -> {
message.setState(FAILED);
mDatabase.synchroniseMessage(message));
})
.onErrorReturn(e -> Observable.just(message));
When I subscribe to the above, I get a Disposable. Normally I'd add it to the CompositeDisposable object and clear that object then the user has moved to a different view (i.e. fragment). However, in this case, I need to keep running this task to make sure the local database is updated with the task results accordingly.
What would be the most appropriate way to handle this situation? I could simply not add the Disposable into my CompositeDisposable object and therefore it wouldn't be unsubscribed from, but it feels like it could cause issues.
P.S. Showing updates to the user is handled through observing the data in an SQLite table. These events are triggered by the synchroniseMessage method. This is a different subscription which I will simply unsubscribe from, so it's not part of the problem.
One disposes of Disposable as soon as he is no longer interested in it.
In your case you are still interested in the stream regardless user navigates to another screen or no, which means you cannot unsubscribe from it. Which means you cannot add it to CompositeDisposable.
This will result in a situation, when your Activity cannot be garbage collected, because of a implicit reference to it from your Subscription, hence you are creating a memory leak situation.
If you have such a use case, I think you have to perform that request on a component, which will be activity lifecycle independent, like Service.

Categories

Resources