In my Android app I have two activities:
DemoActivity
with a button to start the SearchActivity
with an Intent
SearchActivity
The button is a custom ViewGroup:
SearchButton
As soon as the SearchButton
comes to life it registers for lifecycle events (of the corresponding SearchActivity
):
public class SearchButton extends CardView implements
Application.ActivityLifecycleCallbacks {
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Context applicationContext = getContext().getApplicationContext();
if (applicationContext instanceof Application) {
((Application) applicationContext)
.registerActivityLifecycleCallbacks(this);
}
}
// ...
The events are consumed as follows:
// ...
@Override
public void onActivityStarted(Activity activity) {
if (activity instanceof SearchActivity) {
SearchActivity searchActivity = (SearchActivity) activity;
searchActivity.addSomeListener(someListener);
}
}
@Override
public void onActivityStopped(Activity activity) {
if (activity instanceof SearchActivity) {
SearchActivity searchActivity = (SearchActivity) activity;
searchActivity.removeSomeListener(someListener);
}
}
Once the SearchActivity
has been launched I put the app into background and get it back into foreground. The following call stack can be seen:
1. SearchButton.onActivityStarted // triggered by DemoActivity
2. DemoActivity.onStart
3. SearchButton.onActivityStarted // triggered by SearchActivity
4. SearchActivity.addSomeListener
5. SearchActivity.onStart
As you can see the listener is added. This works fine.
As soon as I enable Don't keep activities
in the developer options the call stack looks like this when I get the app foreground again:
1. DemoActivity.onCreate
2. SearchButton.init // Constructor
3. DemoActivity.onStart
4. SearchActivity.onStart
5. SearchButton.onAttachedToWindow
6. DemoApplication.registerActivityLifecycleCallbacks
Here the listener is not added. The desired onActivityStarted
callback triggered by SearchActivity.onStart
is missing.
onPause (): Called as part of the activity lifecycle when an activity is going into the background, but has not (yet) been killed. The counterpart to onResume().
onDestroy() is called before the activity is destroyed.
You’re seeing the onStart
calls from the view only when the activity has been brought to the foreground after being in the background for some time. Currently it’s impossible to see earlier activity events from your activity’s views since the view hierarchy is still being created and the views are not attached to the window yet.
When an activity is initialised from scratch, the view hierarchy isn’t fully attached until after onResume
. This means that once your view’s onAttachedToWindow
is called, onStart
has already been executed. If you exit the activity you mentioned in the question, you should still see the events for onPause
and so on.
Normally if you put an activity to the background by pressing the home button for example, the activity is stopped but not destroyed. It stays in memory with its view hierarchy if there are adequate system resources to do so. When the activity is restored to the foreground, instead of creating it from scratch it calls onStart
and resumes from where it left off, without recreating the view hierarchy.
The “Don’t keep activities” option makes sure that each activity is destroyed right away after it leaves the foreground, making sure that your view’s onAttachedToWindow
is always called after onResume
since the view hierarchy needs to be recreated every time.
Without sharing more code it’s not immediately clear why you need to set the listener within the view. It seems that you need to listen to the activity’s lifecycle method in any case.
If the listener is only tied to the activity’s lifecycle, you might be able to extract it completely out from the view and into the activity.
If it’s tied both to view’s and the activity’s lifecycle, you could try registering the activity lifecycle callbacks in the constructor of the view since a context is already available at that point.
Alternatively you could go for the solution that Google Maps currently has e.g. in MapView. It requires the activity to proxy all lifecycle methods to the view. This might be useful in case your view is very tightly knit with the activity’s lifecycle. You can see the documentation here.
A fourth option is to use a fragment instead of a view since it has its own set of lifecycle methods. Personally I don’t feel quite comfortable with fragments since their lifecycles are potentially even more complicated.
To explain why this is happening, we need to delve into Android’s source code. The things I’m explaining here are specific to this implementation and might differ between SDK versions and even between Android devices due to the manufacturer’s changes. You should not rely on these details in your code. I’ll be using the SDK 23 source code that ships with Android Studio and a Nexus 6P with build MTC19T.
The easiest place to start investigating is the onAttachedToWindow
method. When is it actually called? Its documentation says that it’s called after the view’s surface is created for drawing, but we’re not satisfied with that.
To find out, we set a breakpoint to a view, restart the app so that the activity is recreated, and investigate the first few frames in Android Studio:
"main@4092" prio=5 runnable
java.lang.Thread.State: RUNNABLE
at com.lnikkila.callbacktest.TestView.onAttachedToWindow(TestView.java:18)
at android.view.View.dispatchAttachedToWindow(View.java:14520)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:2843)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1372)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1115)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6023)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:858)
at android.view.Choreographer.doCallbacks(Choreographer.java:670)
at android.view.Choreographer.doFrame(Choreographer.java:606)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:844)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5422)
...
We can see that the first frames are from the view’s internal logic, from the parent ViewGroup, from something called a ViewRootImpl, and then from some callbacks from Choreographer and Handler.
We’re not sure what created those callbacks, but the closest callback implementation is named ViewRootImpl$TraversalRunnable so we’ll check that out:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
There’s the definition, and right below is the callback instance that’s given to Choreographer in this method:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
Choreographer is something that runs on every UI thread on Android. It’s used to synchronise events with the display’s frame rate. One reason to use it is to avoid wasting processing power by drawing things faster than what the display can show.
Since Choreographer uses the thread’s message queue, we couldn’t see this call in the previous frames because the call wasn’t made until Looper handled the message. We can set a breakpoint to this method to see where this call is coming from:
"main@4091" prio=5 runnable
java.lang.Thread.State: RUNNABLE
at android.view.ViewRootImpl.scheduleTraversals(ViewRootImpl.java:1084)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:913)
at android.view.ViewRootImpl.setView(ViewRootImpl.java:526)
- locked <0x100a> (a android.view.ViewRootImpl)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:310)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:85)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3169)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2481)
at android.app.ActivityThread.-wrap11(ActivityThread.java:-1)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5422)
...
If we look into ActivityThread’s handleLaunchActivity
, there’s the call to handleResumeActivity
. Before that is a call to performLaunchActivity
, and in that method are calls to Instrumentation#callActivityOnCreate
, Activity#performStart
and so on.
So there we have our proof that the views aren’t attached until after onResume
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With