Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can a user interaction trigger events before OnResume has finished?

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?

like image 236
stefan.at.wpf Avatar asked Mar 22 '20 23:03

stefan.at.wpf


2 Answers

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
        }
    }
}
like image 64
ianhanniballake Avatar answered Sep 30 '22 02:09

ianhanniballake


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.

like image 29
Anton Malyshev Avatar answered Sep 30 '22 03:09

Anton Malyshev