I have an activity that uploads pictures. Inside this activity I have following observer which is working perfectly:
pictureViewModel.customCreateResult.observeForever { result -> onResponsePostPicture(result!!)}
I need to use observeForever because the user sometimes navigates to other activities. This is working fine and is not the problem. When the user decides to leave this activity with the observer. So when finishing the activity I'm calling:
override fun onDestroy() {
super.onDestroy()
pictureViewModel.customCreateResult.removeObserver{ result -> onResponsePostPicture(result!!)}
}
When for example 2 out of 4 pictures are uploaded and then the user finishes the activity but decides to reopen the activity. I'm getting a respond of the last 2 pictures from the observer. So my removeObserver is not working. What am I doing wrong?
You are not adding and removing same observer each time you are creating anew one . You are passing a lambda which is a new observer every time . Below is an example .
private var observer:Observer<String> = Observer {
onResponsePostPicture(it!!)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main2)
pictureViewModel.customCreateResult.observeForever(observer)
}
override fun onDestroy() {
super.onDestroy()
pictureViewModel.customCreateResult.removeObserver(observer)
}
Alternatively you can use #removeObservers(this) Which will remove all the observers corresponds to Lifecycle Owner.
override fun onDestroy() {
customCreateResult.removeObservers(this)
super.onDestroy()
}
To add to what #ADM said, the issue is specifically that passing a lambda to observeForever/removeObserver creates a new Observer object that the system holds onto. Even if you pass the same lambda instance by holding it as a val, or anything like that, internally it's a new and completely different object.
So by registering an observer in this way, you cannot remove it with removeObserver and it will continue to receive events, and it can create large memory leaks if your lambda has references to things which lead back to something like an Activity.
The documentation doesn't warn you about this, and code completion for observeForever even suggests the lambda version, which is the way the rest of the Kotlin LiveData observer examples are written. It's a huge problem waiting to silently happen, and I wish they'd at least make people aware
Related
In MyViewModel a MutableStateFlow is used to transmit events to the fragment.
When the value of the MutableStateFlow is changed the earlier values are being overwritten inside the coroutine. So never received by fragment.
internal class MyViewModel(application: Application) : AndroidViewModel(application) {
private val myMutableStateFlow = MutableStateFlow<MySealedClass>(MySealedClass.Dummy1())
private fun getData() {
viewModelScope.launch {
//yield()
myMutableStateFlow.value = MySealedClass.Dummy2()
myMutableStateFlow.value = MySealedClass.Dummy3()
}
}
}
internal class MyFragment : Fragment(){
private var uiStateJob: Job? = null
override fun onStart() {
super.onStart()
uiStateJob = lifecycleScope.launch {
myViewModel.getUiFlow().collect {
//do something
}
}
}
}
If yield() is commented Dummy2 event is never received by the Fragment. Dummy 3 is received though.
If yield() is uncommented Dummy2 & 3 are both received.
If the state values are changed outside the coroutine then both Dummy2 and Dummy3 are received.
I need to predictably receive all events in my fragment.
Is there a proper reasoning for this behaviour?
StateFlow is meant to represent a state. Each event is technically a new up-to-date state value, making the previous states obsolete. This type of flow is for situations when only the latest state matters, because its events are conflated. From the docs:
Updates to the value are always conflated. So a slow collector skips fast updates, but always collects the most recently emitted value.
Edit in response to your comment: yield() is a suspend function that forces the suspension of the current coroutine. Therefore it gives a chance to the other coroutine to progress until its next suspension point, this is why in that case the collect is "ready" before the first value is set (and emitted).
However you shouldn't rely on that because it's brittle: if the other coroutine gets modified and has extra suspension points by calling other suspend functions, it might not reach the collect call, and you would be back to the other behaviour.
If you consistently need all events, you have several options:
Switch to a cold flow, which will only start when you collect
Use a Channel (with or without buffer)
Use a SharedFlow and trigger the events start by using onSubscription
All my CameraX initializations reside in FragmentA, and my goal is to navigate to FragmentB depending on some condition that must be verified inside analyze().
When navigating directly from analyze(), through Logcat I can see that FragmentB is loaded correctly but the UI freezes on the camera preview, and unfreezes only when I navigate back to FragmentA. I discovered that under those circumstances FragmentA doesn't go through the rest of its lifecycle correctly, meaning that onDestroyView() and the other methods are called only when I navigate back to it, before immediately beginning a new lifecycle; this leads to cameraExecutor.shutdown() not being called when it's required.
Edit: I updated the code to reflect my latest attempts at finding a solution. I've added a proper callback which at least looks nicer than what I was doing before, but it still doesn't help.
Out of curiosity I've added a Button beside CameraX PreviewView in FragmentA's layout, so that it calls findNavController().navigate(). Lo and behold, clicking directly on it makes it all work as expected, but unfortunately I must do it programmatically without any user input. And if I simulate the button click by calling Button#callOnClick() or Button#performClick() from within the callback it doesn't work again.
class MyAnalyzer(private val callback: () -> Unit) : ImageAnalysis.Analyzer {
override fun analyze(imageProxy: ImageProxy) {
if (foo) {
imageProxy.close()
callback()
}
// do other stuff with imageProxy...
imageProxy.close()
}
}
class FragmentA : Fragment() {
// rest of the code...
private lateinit var cameraExecutor: ExecutorService
override fun onDestroyView() {
super.onDestroyView()
Log.d(TAG, "executor shutdown")
cameraExecutor.shutdown()
Log.d(TAG, "FragmentA destroyed")
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
cameraExecutor = Executors.newSingleThreadExecutor()
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraProviderFuture.addListener(Runnable {
// other CameraX code...
val imageAnalysis = ImageAnalysis.Builder()
.setTargetRotation(rotation)
.build()
.also {
it.setAnalyzer(
cameraExecutor, MyAnalyzer({
findNavController().navigate(R.id.action_fragmentA_to_fragmentB)
})
)
}
// bind to lifecycle of use cases
}, ContextCompat.getMainExecutor(requireContext()))
}
It turns out the answer was under my nose since the beginning but I couldn't see it until a few days ago. At first I was fully convinced that there was something wrong with the camera, because it was the part of the app where the issue appeared to be originating from.
Then I realized that when navigating from FragmentA to FragmentB, the latter begins its lifecycle before that of the first one is completed; I began to wonder if maybe it was actually the fragment's or the Navigation Component's fault, because if that were the case the freeze would have probably been caused by the camera not being released quickly enough during the fragment swap. After some general testing I discovered that apparently that's the way all fragments (and maybe even activities) work, so I gave up the possibility.
After some more tinkering I randomly discovered that if FragmentB was totally blank code-wise (in the Kotlin file) it worked fine, but I really couldn't understand why that was the case. After some more time it finally hit me: I had always failed to realize that it was my code's fault inside FragmentB because I was running an innocent-looking loop which lasted for a very long time, and obviously it was being executed on the main thread freezing the UI.
I knew that you shouldn't execute slow or expensive code on the main thread but all the tutorials always mention network requests or file operations, so I never once took in consideration that even a lenghty loop could have the same side effects. Of course after wrapping the loop in a coroutine the mistery was solved.
Say I have a custom application in class in my Android app that makes a network call, asynchronously, on startup. Then I have a Main activity that needs the results of the network call on startup. I can handle this with a Splash activity that waits on app startup. But when the process is recreated, we go straight to the Main activity, which expects the results of the network call to be there, and it crashes.
See the code below for an example.
What's the best way to handle this? I want to keep the splash screen for normal startup situations. But in the second situation, where the app is recreated, I'm not sure how to handle it. Is there a way to show the splash screen again, and wait, before returning to the recreated Main activity?
class MyApplication : Application() {
private val scope = CoroutineScope(Dispatchers.Main)
companion object {
lateinit var version: Integer
var startupFinishedListener: (() -> Unit)? = null
}
override fun onCreate() {
super.onCreate()
scope.launch {
version = getVersionFromNetwork() //Fake suspending network call
startupFinishedListener?.invoke()
}
}
}
class SplashScreen : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.splash)
MyApplication.startupFinishedListener = {
startMainActivity()
}
}
fun startMainActivity() {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
}
}
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
//This line will crash if we are coming from an activity recreation
Timber.d("Version is: ${MyApplication.version}")
}
}
I would prefer not to solve this by doing work in onResume in all of my activities instead of onCreate(). Ideally I could tell Android to launch my Splash activity before it launches the restored activity.
You will have to handle this in your onResume() lifecycle. You can create an onResume and then call the SplashActivity intent and finish() your MainActivity. This way it will just go back to Splash Screen where it will load the data and call the Main Activity again
override fun onResume() {
super.onResume()
val intent = Intent(this,SplashActivity)
startActivity(intent)
}
"This looks like an Architectural Problem."
Calling an API into application class and registering it's listener to your activities is a bad idea!
There are few options for you to handle your requirement:
If the data you want is really that important in MainActivity, you can call the API in MainActivity onCreate() itself with some loading indicator before your actual data is loaded.
What will happen if process restarts in this case?
Your MainActivity will recreate and call the API again. So, you're good.
If you're required to have that data as soon as your MainActivity starts, without waiting, you need to do caching. Call your API in splash activity and save the data to a file, shared preference or database. Then access cached data in your MainActivity.
What will happen if process restarts in this case?
Since you've already cached your data into a persitent storage, it'll be there even after process restart. So, you're good to get your data back.
Bonus
If you don't want to use your cached data everytime you open your app, just clear the cache in your SplashActivity and call the API everytime to have the latest data with you before moving to MainActivity.
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
}
}
I have application that has MainActivity, that starts InvoiceActivity and that starts InvoicePaymentActivity which finally starts PaymentSuccessActivity.
I started using architecture components and it seems to work fine, bud I have found problem when starting MainActivity from PaymentSuccessActivity.
Up until now, I would just start new Intent and the app would "reset" to main screen. With ViewModel I am getting "Cannot add the same observer with different lifecycles".
I have found 2 solutions, but cannot think which one is better:
Subscribe to observer in onResume, unsubscribe onPause
Finish all previous activities except MainActivity after the next one is started. So when I just finish PaymentSuccessActivity, user will be on MainActivity. This has a drawback of user navigating backwards...
But it seems I cannot add the observer again... how can I
unsibscribe/subscribe? My code does not work right now...
override fun onResume() {
super.onResume()
viewModel.intercom.observe(this, observer)
}
override fun onPause() {
super.onPause()
viewModel.intercom.removeObserver { observer }
}
So this whole problem was base on using anonymous Observer class. Once I created my observer class implementing observer interface, the app startet to work fine, without needing to manually observer/remove. Can anyone explain why this is issue?