In one of my Fragments, I am registering an OnFocusChangeListener for an EditText in onResume():
override fun onResume() {
super.onResume()
editText.setOnFocusChangeListener {
// do something here
}
}
I am registering the listener in onResume() because if I would set it in an earlier lifecycle method, it would be triggered on every configuration change. Setting it in onResume() makes sure that the focus that existed before a configuration change is already restored before the listener is registered, so the listener won't automatically fire after a configuration change / focus restore.
Now I fear that I am maybe registering this listener too late. So my question is: Can a user interaction already lead to focus for an element before or while onResume() is executed? (That would mean, that I would loose this focus event, because I am setting up the listener during onResume()). Or more general: is user interaction already possible while onResume() is being executed? The Fragment documentation says about onResume():
Called when the fragment is visible to the user and actively running.
It's clear what "visible to the user" means, but what exactly means "actively running"? Does this already mean accepting user input? Or is user input first accepted after onResume() has finished?
Focus restoration is done in the Activity's onRestoreInstanceState(), which is done separately from when Fragment's restore their own View's state (that would be in the Fragment's onViewStateRestored()).
As per the onRestoreInstanceState() documentation, it is called between the Activity's onStart() and onPostCreate() (which runs prior to onResume() and onPostResume() - onPostResume() is when Fragment's get their onResume() callbacks).
This means that you are correct in that there is no callback available at the Fragment level prior to onResume() where the focus is set correctly prior to that method being called.
That being said, yes, users can interact with a Fragment prior to it reaching the resumed state. For example, ViewPager2 (as well as ViewPager 1 when using BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) keep the non-selected fragments (i.e., those not in the middle of the screen) in the STARTED state. Through the power of multi-touch, users could drag a page over slightly and then use another finger to tap on a view that is partially visible. You'd see the same behavior if you use setMaxLifecycle() with Lifecycle.State.STARTED yourself (which is what those are doing under the hood) - the fragment is interactable, but not RESUMED.
In most general cases, however, if you're not using any of the above APIs, the Fragment lifecycle will generally match the Activity lifecycle. And an activity, as per the ActivityThread source code, does run its updates all in the same handleStartActivity() method.
It should be noted that every time the activity is destroyed, you will get a callback to your OnFocusChangeListener with hasFocus of false as the View is removed from the Activity (which always loses the View's focus). This happens after the state is saved, the View's focus state isn't actually lost, it is just something you already need to handle in your callback, usually by checking isStateSaved() and ignoring focus loss after the state is saved and checking isRemoving() if you are manually removing / replacing the Fragment (i.e., by doing a replace() operation).
Given that you already will have to have logic in your listener to avoid handling the hasFocus of false events post destruction, the 100% correct case for handling gaining focus would involve saving your own focused state (i.e., true or false for a specific view) in your saved instance state and only running your logic if the hasFocus is changing from what you already have saved. This means that you'd restore your saved instance state earlier in the Fragment's lifecycle (say, in onViewStateRestored() method that Fragments provide) and add your listener there. Then, your logic could safely ignore callbacks with the same focus:
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
// Restore your member variable of focus
focused = savedInstanceState?.getBoolean("HAS_FOCUS", false) ?: false
editText.setOnFocusChangeListener { _, hasFocus ->
if (focused == hasFocus) {
// ignore
return
}
focused = hasFocus
if (hasFocus) {
// We gained focus
} else if (!isStateSaved() && !isRemoving()) {
// We lost focus
}
}
}
Looking at FragmentManager source code, performResume call which triggers onResume is executed immediately after the fragment is started (and onStart is called):
https://android.googlesource.com/platform/frameworks/support/+/84448d71fda0a24ba5d60fe9368ac47b97564c88/fragment/src/main/java/androidx/fragment/app/FragmentManagerImpl.java#926
Starting the fragment is required for user interaction and no interactions can happen between onStart and onResume calls, since they can execute only on the same Main thread.
So, yes, no user input is possible before onResume.
Related
I'm working with ViewModels and LiveData in an Android app, and I want to use them to keep track of data for an Activity even when the screen is rotated. This works quite well, but there's one issue I can't fix. In the Activity's onCreate method, I register an observer for a LiveData containing a list of objects, which should just add a Fragment to the activity if the data is loaded. Then, I only reload the data if savedInstanceState is null, which should stop it from being reloaded in the event of a screen rotation. This looks like this:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
model = ViewModelProviders.of(this).get(MainActivityModel.class);
observeList();
if (savedInstanceState == null) {
loadList(); //Reload the list and call postValue() on the LiveData.
}
}
private void observeList() {
model.getList().observe(this, new Observer<ArrayList<Object>>(){
#Override
public void onChanged(#Nullable ArrayList<Object> objects) {
//Add list fragment whenever data changes.
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, ListFragment.getInstance(list))
.commit();
}
});
}
The way I understand it, the ListFragment should only be shown when data changes. However, after some debugging, it seems the onChanged method in the observeList method gets called every single time the screen is rotated. Additionally, I checked whether the actual list was changed and it was exactly the same, no difference at all and there was never even a postValue() or setValue() method call. Therefore, I have no idea why the onChanged method would be called on screen rotation.
This also only happens when the list contains something. When the app is started up, the list has a length of 0 before loadList() is called. When the observer is registered at this state, the onChanged method is not called.
My best guess is that the onChanged method gets triggered either because the list is not empty when the observer is registered so the observer could think there is a change in data. Can anyone explain why this might be happening?
This is working as intended. From livedata overview:
To ensure that the activity or fragment has data that it can display as soon as it becomes active. As soon as an app component is in the STARTED state, it receives the most recent value from the LiveData objects it’s observing. This only occurs if the LiveData object to be observed has been set.
Although there's missing code snippets - loadList should be a part of ViewModel itself, not a method of activity.
I had the same problem and I found a solution in this code lab: Training LiveData
Usually, LiveData delivers updates to the observers only when data changes. An exception to this behavior is that observers also receive updates when the observer changes from an inactive to an active state.
When the (...) fragment is re-created after a screen rotation, it moves from an inactive to an active state. The observer in the fragment is re-connected to the existing ViewModel and receives the current data.
So the solution is basically to create some method in the View Model that resets the value of the variable that triggers the observer (in your case, the list).
when a screen is rotated, the following callback methods in the activity lifecycle are called
onSaveInstanceState()
onPause()
onStop()
onCreate()
onStart()
onRestoreInstanceState()
onResume()
it would help if you kept in mind that the live data observer is lifecycle aware, meaning that it will observe when the UI is in an active state and will stop observing when the UI is not in an active state. when the onStop method is called the live data observer stops listening and when the onCreate method is called again the live data observer starts to observe again. that is why the observer is reloaded .
I have the following code in Activity
#Override
public void onPause() {
super.onPause();
if (isFinishing()) {
final LiveData<StickyNoteConfig> stickyNoteConfigLiveData = StickyNoteConfigRepository.INSTANCE.getStickyNoteConfig(mAppWidgetId);
stickyNoteConfigLiveData.removeObservers(this);
stickyNoteConfigLiveData.observe(this, stickyNoteConfig -> {
// Weird, I still can receive call back.
// I thought "this" is no longer active?
});
}
}
I feel puzzled that, Observer is still being triggered, although this activity is already in onPause state? According to https://developer.android.com/reference/android/arch/lifecycle/Lifecycle.State#STARTED
Started state for a LifecycleOwner. For an Activity, this state is
reached in two cases:
after onStart call;
right before onPause call.
May I know why it is so?
According to LiveData reference,
LiveData is a data holder class that can be observed within a given lifecycle. This means that an Observer can be added in a
pair with a LifecycleOwner, and this observer will be notified about
modifications of the wrapped data only if the paired LifecycleOwner
is in active state. LifecycleOwner is considered as active, if its
state is STARTED or RESUMED.
An observer added with a Lifecycle will be automatically removed if the corresponding Lifecycle moves to DESTROYED state.
Now, according to your situation here, LiveData receives updates for your observer (your activity) in onPause() method, because your observer is not already in DESTROYED state.
So LiveData is still active for receive updates according to these methods:
onActive() :
Called when the number of active observers change to 1 from 0.
This callback can be used to know that this LiveData is being used thus should be kept up to date.
&
onInactive() :
Called when the number of active observers change from 1 to 0. This does not mean that there are no observers left, there may still be observers but their lifecycle states aren't STARTED or RESUMED (like an Activity in the back stack).
You can check if there are observers via hasObservers().
So when does observer (your activity) gets DESTROYED state?
For default implementation of LifeCycleOwner indicates that activity gets it's DESTROYED state once onDestroy() method is executed & after onPause() it follows reverse order of LifeCycle state RESUMED -> STARTED -> CREATED -> DESTROYED.
Check this lifecycle graph.
Hope it helps !
https://medium.com/#hanyuliu/livedata-may-lose-data-2fffdac57dc9 has logs to explain this issue. And just like #Vairavan said,in pause state activity can be partially visible.
public enum State {
DESTROYED,
INITIALIZED,
CREATED,
STARTED,
RESUMED;
public boolean isAtLeast(#NonNull State state) {
return compareTo(state) >= 0;
}
}
so, observer's isAlive judge according to isAtLeast(STARTED). when OnPause is called, activity does not become DESTROYED.
This becomes obvious when onPause and onStop is related to UI side effects. An activity is paused and not stopped if any part of the activity window is still visible. This could happen when another activity is shown on top of the paused activity as a dialog. In this case, more often than not, developers would like the paused activity's UI to still be updated even though it is only partially visible. LiveData's update in this paused state helps do that.
There are other cases like multi-window feature. One activity could be paused while the user is interacting with another app/activity in a different window. Paused activity could be a video playback with active updates and should be updated even though user is interacting with another app. Note that this implementation is changing from Android P for foldable phones where multiple activities/windows could be in resumed state (that is not the concern for live data updates anyways). https://android-developers.googleblog.com/2018/11/get-your-app-ready-for-foldable-phones.html
Normally, onPause (without onStop) will happened when a partially of screen is cover by other screen (eg: notification from other app, or open a transparent activity).
Because, whole screen is not cover, so we may see some part of the current screen.
Therefore, I think LiveData may still need to notify in on pause state to make sure user always see the latest data.
If user go to background, or navigate to other screen, then the whole of screen is cover, we dont need to update UI because user dont see it. So LiveData dont need to notify in onStop state
Helpful answers:
Android: Scenario where onPause is called but not onStop?
LiveData only updates app component observers that are in an active lifecycle state.
If anyone wants to perform any lifecycle-related work they must override onActive() and onInActive() methods.
eg:
public class StockLiveData extends LiveData<BigDecimal> {
......
#Override
protected void onActive() {
}
#Override
protected void onInactive() {
}
}
Important Line:
An observer added with a Lifecycle will be automatically removed if
the corresponding Lifecycle moves to DESTROYED state.
I have the following security requirement for my app :
Verify that the app removes sensitive data from views when backgrounded.
My question is, Does Android remove data from views when backgrounded ?
What i know as per android documentation is that :
By default, the system uses the Bundle instance state to save information about each View object in your activity layout (such as the text value entered into an EditText widget).
One solution is that i can clear all views then use below approach to save state
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
// Save the user's current game state
// Always call the superclass so it can save the view hierarchy state
super.onSaveInstanceState(savedInstanceState);
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // Always call the superclass first
// Check whether we're recreating a previously destroyed instance
if (savedInstanceState != null) {
// Restore value of members from saved state
} else {
// Probably initialize members with default values for a new instance
}
// ...
}
But i have no idea of whether Android os clears data in views.Any help will be appreciated.
Thanks
Android does not automatically recycler your view as soon as your application moves to background.
The system kills processes when it needs to free up RAM; the likelihood of the system killing a given process depends on the state of the process at the time.
It just recycle views if it decided to kill them or if it's in need for memory. Or upon a configuration change.
A user expects an activity’s UI state to remain the same throughout a configuration change, such as rotation or switching into multi-window mode. However, by default the system destroys the activity when such a configuration change occurs, wiping away any UI state stored in the activity instance.
Overall I think you had the right approach.
And just use onStop() (and not onPause() because of some Dialogs and other intercations that removes focus from your Activity) to clear your views if you want to.
Take a look at the android activity lifecycle https://developer.android.com/guide/components/activities/activity-lifecycle
One way to make sure all your data is cleared is to override the onPause function and clear your data manually. onPause is called when your application moves to the background.
No, it does not remove data because, according to the lifecycle of Android, when you do background the views, onPause() and onResume() is called and your views and its instances are in onCreate() method.
So, by launching the views from background, it does not remove your data of views.
When orientation of screen changes, I have read many a times that in order to save data of edit text and text view or any of the radio button I have to use onSaveInstanceState() method.
But when I'm changing the screen orientation my data of edit text, text view and radio button are not getting erased.
So what is the main purpose of using onSaveInstanceState() method. Why do we have to use it if my data are preserved safely ?
Some Views/properties may be handled by default. You could go rooting through the docs to find out exactly which ones and how they are taken care of but...
I would recommend that you take manual control of these save/loads though to ensure that things are handled as you want them to be to avoid edge case bugs and also so you can then persist certain settings/properties/states if need be.
It really depends on the complexity and contents of your Activity/Fragment/Layout/View/Preference etc etc and if you even really need to remember the state things were a few moments ago.
onSaveInstanceState(Bundle outState)
This method is called before an activity may be killed so that when it comes back some time in the future it can restore its state.
Do not confuse this method with activity lifecycle callbacks such as onPause, which is always called when an activity is being placed in the background or on its way to destruction, or onStop which is called before destruction. One example of when onPause and onStop is called and not this method is when a user navigates back from activity B to activity A: there is no need to call onSaveInstanceState on B because that particular instance will never be restored, so the system avoids calling it. An example when onPause is called and not onSaveInstanceState is when activity B is launched in front of activity A: the system may avoid calling onSaveInstanceState on activity A if it isn't killed during the lifetime of B since the state of the user interface of A will stay intact.
onRestoreInstanceState(Bundle savedInstanceState)
This method is called after onStart() when the activity is being re-initialized from a previously saved state.
Most implementations will simply use onCreate(Bundle) to restore their state, but it is sometimes convenient to do it here after all of the initialization has been done or to allow subclasses to decide whether to use your default implementation. The default implementation of this method performs a restore of any view state that had previously been frozen by onSaveInstanceState(Bundle).
On the default handling of your views with this pair of methods:
The default implementation takes care of most of the UI per-instance state for you by calling View.onSaveInstanceState() on each view in the hierarchy that has an id, and by saving the id of the currently focused view (all of which is restored by the default implementation of onRestoreInstanceState). If you override this method to save additional information not captured by each individual view, you will likely want to call through to the default implementation, otherwise be prepared to save all of the state of each view yourself.
Is there a callback that gets always called after onResume()? I'd need that, because AFAIK, after onResume(), every View in the layout has been rendered, so I can measure their dimensions.
Thanks.
Activity | Android Developers
protected void onPostResume ()
Since: API Level 1
Called when activity resume is complete (after onResume() has been called). Applications will generally not implement this method; it is intended for system classes to do final setup after application resume code has run.
Derived classes must call through to the super class's implementation of this method. If they do not, an exception will be thrown.
You might also be interested in (in the same link):
public void onWindowFocusChanged (boolean hasFocus)
Since: API Level 1
Called when the current Window of the activity gains or loses focus. This is the best indicator of whether this activity is visible to the user. The default implementation clears the key tracking state, so should always be called.
Note that this provides information about global focus state, which is managed independently of activity lifecycles. As such, while focus changes will generally have some relation to lifecycle changes (an activity that is stopped will not generally get window focus), you should not rely on any particular order between the callbacks here and those in the other lifecycle methods such as onResume().
As a general rule, however, a resumed activity will have window focus... unless it has displayed other dialogs or popups that take input focus, in which case the activity itself will not have focus when the other windows have it. Likewise, the system may display system-level windows (such as the status bar notification panel or a system alert) which will temporarily take window input focus without pausing the foreground activity.
Parameters
hasFocus Whether the window of this activity has focus.