Unable to trigger an Android Activity from RxJava2 chain [Android][RxJava] - android

I have been trying to write an RxJava2 chain and trigger an Android Activity from this chain. The idea is to get an Observable on which I can publish the result of the activity using an interface callback. Here is the code that I call from my Activity to launch the sso flow.
final Disposable disposable = userAuthManager
.initateSignup(this)
.map(ssoResponse -> {
<Do some stuff off the main thread>
return ssoResponse;
})
.subscribe(....)
UserAuthManager.initateSignup(Activity activity) goes like this:
override fun initateSignup(starterActivity: Activity): Single<SSOResponse> {
return ssoService.startSSO(<someData>, starterActivity)
.map {
....... Some business logic and then returning the response
// After handling, finally returning the SSOResponse ...
it
}
}
Finally SSOService.startSSO(<someData>, starterActivity) goes like this:
override fun startSSO(oldUserSessionJID: String?, starterActivity: Activity): Single<SSOResponse> {
val intent = Intent(starterActivity, <MyTargetActivity>::class.java)
starterActivity.startActivity(intent)
// The below statement prints `true`
Timber.d("This is main thread: " + (Looper.myLooper() == Looper.getMainLooper()))
listenableFuture = SettableFuture.create()
return Single.fromFuture(listenableFuture)
}
In the code above for startSSO, I return a Single from a SettableFuture which I set once I get the response inside the activity.
Issue: MyTargetActivity launches with a black screen and nothing renders on it. Also, the debug points inside the activity behave weirdly. Sometimes they work and sometimes they don't. I have tried launching activity by directly calling SSOService.startSSO(<someData>, starterActivity) from a button click and it works fine. Also, I was thinking that the issue might be related to activity being launched on a non-UI thread and hence I logged that as well. The activity seems to be launched on a UI thread based on the log statement. What is the problem here?
PS: I have removed observeOn() and subscribeOn() calls to make things simple and assume everything runs on the UI thread.

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.

withContext after a CoroutineScope.launch - will CoroutineScope.launch block?

I just stumbled over this code:
fun addHeaderAndSubmitList(list: List<SleepNight>?) {
adapterScope.launch {
val items = when (list) {
null -> listOf(DataItem.Header)
else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
}
// isn't there any code required to wait for the
// adapterScope.launch coroutine to finish?
withContext(Dispatchers.Main) {
submitList(items)
}
}
}
found in this file of the google sleeptracker example.
I already added my question as comment in the code example. I am new to coroutines but to my knowledge adapterScope.launch is non-blocking, so adapterScope.launch might not be finished until
withContext(Dispatchers.Main) {
submitList(items)
}
is reached? Am I wrong about this? If not, how to fix it?
See launch.
Launches a new coroutine without blocking the current thread [...]
Here's what happens:
addHeaderAndSubmitList uses launch to start some asynchronous work. The work will finish naturally or will be terminated when adapterScope's lifecycle ends. Meanwhile addHeaderAndSubmitList finishes immediately.
Whatever is inside launch {} runs sequentially. submitList(items) is called after val items = .... Each happens effectively on a different thread, but the order is guaranteed.
The code inside launch { } runs sequentially in a blocking fashion inside the adapterScope, meaning all the code above the withContext(Main) is run and finished before switching contexts to submit the list to the adapter on the main thread.
The entire code block is likely running in an Default or IO context, so it runs in a blocking fashion outside the main thread, until it reaches the withContext(Main) to post the results to the main thread.

Android ViewModel observer not called from background

I'm working on small android app using MVVM pattern.
My issue is that my ViewModel observer in MyActivity not called from the background. I need it to be called even if the app is in background to show system Notification to the user that app calculation process is done and the result is ready.
This is the current implementation located in onCreate in MyActivity:
mainActivityViewModel.getTestResult().observe(MainActivity.this, new Observer<String>() {
#Override
public void onChanged(#Nullable String blogList) {
Toast.makeText(getApplicationContext(), "test...", Toast.LENGTH_SHORT).show();
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)){
//The app is in foreground - showDialog
}else{
//The app is in background - showNotification
}
}
For now, this observer will be called only if the app is in foreground - if the process done while app was in foreground - 'showDialog' will trigger, if the app was in background - the showNotification will trigger - but only after I will open the app again. It's not the behaviour that I try to achieve. Please help! Thanks.
onChanged will only be called if the Activity's current Lifecycle state is at least STARTED. onPause gets called when you leave the Activity, which means it's not at least STARTED.
LiveData is simply not suitable for the behavior you're trying to achieve.
I would recommend you to use a foreground Service instead. Especially if the mentioned "calculation process" is something that the user should be aware of.
edit:
Let's say you're performing some potentially long running task in the background and you want to continue this task even if the user would leave or even close your Activity. Then using a Service is a good option, and especially a foreground Service if the task is the result of a user action. For example, the user clicks an "upload" button, a foreground Service performs the task and the associated Notification says "Upload in progress".
You have the option to either
Always show a new Notification when the task is complete, regardless of if the Activity is shown or not. This is pretty common.
Only show the Notification if the Activity is not currently started, and if it is started, show something in the Activity view instead.
In order to do the latter option, you need to know the current status of the Activity's Lifecycle. You want to be able to do the following check from your service somehow: getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)
The best way to communicate between an Activity and Service is binding to the Service and extending the Binder class in the Service.
After binding, you may store the Activity Lifecycle status in a variable in the Service, or even provide the Activity itself to the Service.
I guess your getTestResult() in ViewModel returning some live data.
So first of all, you are assigning your real data with LiveData using .setValue(some_data) method. And it is working fine while app is open. Btu when your app is in background. You need to use .postValue(some_data) method to assign data with that LiveData.
Check difference below:
setValue()
Sets the value. If there are active observers, the value will be dispatched to them. This method must be called from the main thread.
postValue()
Posts a task to a main thread to set the given value. If you called this method multiple times before a main thread executed a posted task, only the last value would be dispatched.
Conclusion, the key difference would be:
setValue() method must be called from the main thread. But if you need set a value from a background thread, postValue() should be used.
I saw this question researching for the same issue and even though it was asked 2 years ago I was able to let LiveData notify the observer even though the Fragment (or in question's case, an Activity) is either paused or stopped, so I am posting my solution here.
The solution is for a fragment, but can be adapted to activities as well.
On the fragment:
class MyFragment: Fragment() {
private var _lifecycleWrapper: LifecycleOwnerWrapper? = null
val activeLifecycleOwner: LifecycleOwner
get() {
if (_lifecycleWrapper == null)
_lifecycleWrapper = LifecycleOwnerWrapper(viewLifecycleOwner)
return _lifecycleWrapper!!
}
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
// On the livedata, use "activeLifecycleOwner"
// instead of "viewLifecycleOwner"
myLiveData.observe(activeLifecycleOwner) { value ->
// do processing even when in background
}
}
override fun onDestroyView() {
super.onDestroyView()
_lifecycleWrapper = null
}
}
LifecycleOwnerWrapper:
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
/**
* A special lifecycle owner that lets the livedata
* post values even though the source lifecycle owner is in paused or stopped
* state. It gets destroyed when the source lifecycle owner gets destroyed.
*/
class LifecycleOwnerWrapper(sourceOwner: LifecycleOwner):
LifecycleOwner, LifecycleEventObserver
{
private val lifecycle = LifecycleRegistry(this)
init
{
sourceOwner.lifecycle.addObserver(this)
when (sourceOwner.lifecycle.currentState)
{
Lifecycle.State.DESTROYED -> lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
Lifecycle.State.CREATED -> lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
Lifecycle.State.STARTED -> lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START)
Lifecycle.State.RESUMED -> lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
else ->
{
// do nothing, the observer will catch up
}
}
}
override fun getLifecycle(): Lifecycle
{
return lifecycle
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event)
{
if (event != Lifecycle.Event.ON_PAUSE && event != Lifecycle.Event.ON_STOP)
lifecycle.handleLifecycleEvent(event)
}
}
The only thing you need to do is to not call this after onDestroy (or for viewLifecycleOwner, after onDestroyView) otherwise the lifecycle owner will be stale.
What you are trying to do is possible but not in the way you are doing it.
The whole purpose of the LiveData API is to link the data layer with the UI in a life cycle aware manner, so when the app is not in foreground then the observer knows that and stop updating the UI.
The first argument on the observer is the lifecycle.
This is a great improvement because without it the crashes because UI was not available were too often or it was too complex to control manually (boilerplate, edge cases, etc).
Service is not a good idea because the services can be killed by the DALVIK or ANT machine if the memory is needed for the foreground app. Services are not in the foreground but that doesn't mean that are bound to background neither that are guaranteed to be working for a undeterminated span of time.
For doing what you wish use the WorkManager. The WorkManager allows you to schedule jobs with or without conditions and from there you are gonna be able to send a Notification to the user.
You can try for a combination of Workmanager and Viewmodel to achieve an foreground/background app functionality.
For this use the Activity life cycle:
Use the onResume method to remove any WorkManager and star using the ViewModel
Use the onPause method to star the WorkManager
To handle the declaration, you can edit or dismiss the declaration from inside the function in your ViewModel class where the data was successfully retrieved.
private fun dataShow(list: List<String>) {
//Notification cancel
NotificationManagerCompat.from(getApplication()).cancel(30)
if (list.isNotEmpty()) {
data.value = list
progressHome.value = false
} else {
progressHome.value = true
}
}

Android LiveData event sequence guarantees?

I have the following code in my Fragment, subscribing to my ViewModel's LiveData events.
viewModel.successfullyAddedEvent.observeEvent(this){
// do result handling by shared view model to the calling fragment
result.successfullyAddedEvent.postValue(Event(it))
findNavController().navigateUp()
}
viewModel.successfullyEditedEvent.observeEvent(this){
// do result handling by shared view model to the calling fragment
result.successfullyEditedEvent.postValue(Event(it))
findNavController().navigateUp()
}
viewModel.exitRequestedEvent.observeEvent(this){
when(it){
ExitReason.GetDetailsFailed -> {
Toast.makeText(context, R.string.details_load_error, LENGTH_SHORT).show()
}
ExitReason.UserCanceled -> { /* happy path */ }
}
findNavController().navigateUp()
}
I have three places in this code where I'm calling navigateUp and I'd like to unify the navigation a bit.
It seems like it would be a code improvement to emit an ExitRequestedEvent from each case (successful add, successful edit, error case, user cancellation) and remove calls to navigateUp from those events, but I'm not sure about guarantees on receipt order. That is, if I was to post values for a successfullyAddedEvent and then an exitRequestedEvent in my viewmodel like so:
// yay my thing happened successfully, emit events
successfullyAddedEvent.postValue(Event(contentAdded))
exitRequestedEvent.postValue(Event(ExitReason.AddSuccessful))
can I be guaranteed that I'll handle the add event (which sets the result on the shared result ViewModel) before I exit this fragment, so that the calling fragment always has a result?
According to the documentation, postValue just means posting the task to main thread if you are not on it, so unless you have setValue somewhere in your code as well, the order should be guaranteed.
If you want to absolutely guarantee the order (even though not necessary), you can use setValue() for that, but you have to make sure it happens on the main thread.

Categories

Resources