Android | jetpack’s new gesture for handling fallback events — onbackpresseddispatcher

Time:2022-5-10
Android | jetpack's new gesture for handling fallback events -- onbackpresseddispatcher

Like attention, no longer get lost, your support is of great significance to me!

🔥 Hi, I’m ugly. this paperIntroduction to “Android route” — from zero to infinityIt has been included. Here are the notes of Android advanced growth route & blog. Welcome to grow up with Peng Chou Chou. (contact information in GitHub)


preface

  • fromandroidx.activity 1.0.0At first, Google introduced the onbackpresseddispatcher API to handle fallback events, aiming to optimize the processing of fallback events: you can define fallback logic anywhere, rather than relying on activity #onbackpressed();
  • In this article, I will introduce the usage method, implementation principle and application scenario of onbackpresseddispatcher. If you can help, please be sure to praise and pay attention, which is really very important to me.
  • The relevant code of this article can be downloaded fromDemoHall·HelloAndroidXDownload View.

catalogue

Android | jetpack's new gesture for handling fallback events -- onbackpresseddispatcher


Pre knowledge

The content of this article will involve the following pre / related knowledge. I have prepared it for you. Please enjoy it~


1. General

  • What problems does onbackpresseddispatcher solve:In activity, it can be handled through the callback method onbackpressed(), while fragment / view has no direct callback method. Now we can use onbackpresseddispatcher instead of activity #onbackpressed() to implement fallback logic more gracefully.

  • The overall processing flow of onbackpresseddispatcher:The distributor adopts the responsibility chain design mode as a whole, and the callback objects added to the distributor will become a node in the responsibility chain. When the user triggers the return key, the responsibility chain will be traversed in sequence. If the callback object is enabled, the fallback event will be consumed and the traversal will be stopped. If the last event is not consumed, it is returned to activity #onbackpressed() for processing.

  • Comparison of onbackpresseddispatcher with other schemes:Before onbackpresseddispatcher, we can only handle fallback events through “trickery”:

    • 1. Define the callback method in the fragment and pass the callback event from the activity #onbackpressed() (disadvantage: increase the coupling relationship between activity and fragment);
    • 2. Set key listening setonkeylistener in the fragment root layout (disadvantage: inflexible & multiple fragment listening conflicts).

2. What APIs does onbackpresseddispatcher have?

There are the following APIs, which are easy to understand.Addcallback (lifecycle owner, callback) will enter lifecycle in the lifecycle owner State. Only when it is in the started state will it join the distribution responsibility chain, and enter lifecycle in the lifecycle owner State. When in stop status, it will be removed from the distribution responsibility chain.

1. Add callback object
public void addCallback(OnBackPressedCallback onBackPressedCallback)

2. Adds a callback object to associate with the specified lifecycle holder
public void addCallback(LifecycleOwner owner, OnBackPressedCallback onBackPressedCallback)

3. Determine whether there are enabled callbacks
public boolean hasEnabledCallbacks()

4. Fallback event distribution portal
public void onBackPressed()

5. Constructor (parameter is final callback)
public OnBackPressedDispatcher(@Nullable Runnable fallbackOnBackPressed) {
    mFallbackOnBackPressed = fallbackOnBackPressed;
}

3. Onbackpresseddispatcher source code analysis

Onbackpresseddispatcher doesn’t have much source code. I’ll start with the problem and help you sort out the internal implementation principle of onbackpresseddispatcher:

3.1 how does the activity distribute events to onbackpresseddispatcher?

A: the dispenser object is combined inside the componentactivity. The return key callback onbackpressed() will be directly distributed to onbackpresseddispatcher #onbackpressed(). In addition, the fallback logic of the activity itself is encapsulated as runnable and handed over to the distributor for processing.

androidx.activity.ComponentActivity.java

private final OnBackPressedDispatcher mOnBackPressedDispatcher =
    new OnBackPressedDispatcher(new Runnable() {
        @Override
        public void run() {
            //Fallback logic of activity itself
            ComponentActivity.super.onBackPressed();
        }
});

@Override
@MainThread
public void onBackPressed() {
    mOnBackPressedDispatcher.onBackPressed();
}

@NonNull
@Override
public final OnBackPressedDispatcher getOnBackPressedDispatcher() {
    return mOnBackPressedDispatcher;
}

3.2 what is the processing flow of onbackpresseddispatcher?

A: the distributor adopts the responsibility chain design mode as a whole, and the callback objects added to the distributor will become a node in the responsibility chain. When the user triggers the return key, the responsibility chain will be traversed in sequence. If the callback object is enabled, the fallback event will be consumed and the traversal will be stopped. If the last event is not consumed, it is returned to activity #onbackpressed() for processing.

OnBackPressedDispatcher.java

//Final callback: activity #onbackpressed()
@Nullable
private final Runnable mFallbackOnBackPressed;

//Chain of responsibility
final ArrayDeque<OnBackPressedCallback> mOnBackPressedCallbacks = new ArrayDeque<>();

//Constructor
public OnBackPressedDispatcher() {
    this(null);
}

//Constructor
public OnBackPressedDispatcher(@Nullable Runnable fallbackOnBackPressed) {
    mFallbackOnBackPressed = fallbackOnBackPressed;
}

//Determine whether there are enabled callbacks
@MainThread
public boolean hasEnabledCallbacks() {
    Iterator<OnBackPressedCallback> iterator = mOnBackPressedCallbacks.descendingIterator();
    while (iterator.hasNext()) {
        if (iterator.next().isEnabled()) {
            return true;
        }
    }
    return false;
}

Entry method: each callback method in the responsibility chain can be called only when the previous callback is not enabled.
If it is not enabled, it will eventually be called back to mfallbackonbackpressed
@MainThread
public void onBackPressed() {
    Iterator<OnBackPressedCallback> iterator = mOnBackPressedCallbacks.descendingIterator();
    while (iterator.hasNext()) {
        OnBackPressedCallback callback = iterator.next();
        if (callback.isEnabled()) {
            callback.handleOnBackPressed();
            //Consumption
            return;
        }
    }
    if (mFallbackOnBackPressed != null) {
        mFallbackOnBackPressed.run();
    }
}

3.3 is the callback method executed in the main thread or sub thread?

A: in the main thread, the entry method activity#onbackpressed() of the distributor is executed in the main thread, so the callback method is also executed in the main thread. In addition, the addcallback () method to add a callback also requires execution in the main thread, and the non concurrent security container arraydeque is used inside the distributor to store the callback object.

3.4 question 4: can onbackpressedcallback be added to different distributors at the same time?

A: Yes.

3.5 how to fallback after adding the fragment transaction returning to the stack?

A: the fragment manager also returns the transaction to onbackpresseddispatcher for processing. First, during fragment attach, a callback object will be created and added to the distributor, and the transaction returning to the top of the stack will pop up during callback processing. However, the initial state is not enabled. Only when the transaction is added to the return stack will the callback object be modified to the enabled state. The source code is as follows:

FragmentManagerImpl.java

//3.5.1 distributor and callback object (the initial state is not enabled)
private OnBackPressedDispatcher mOnBackPressedDispatcher;
private final OnBackPressedCallback mOnBackPressedCallback =
    new OnBackPressedCallback(false) {
        @Override
        public void handleOnBackPressed() {
            execPendingActions();
            if (mOnBackPressedCallback.isEnabled()) {
                popBackStackImmediate();
            } else {
                mOnBackPressedDispatcher.onBackPressed();
            }
        }
    };

//3.5.2 add callback object addcallback
public void attachController(@NonNull FragmentHostCallback host, @NonNull FragmentContainer container, @Nullable final Fragment parent) {
    if (mHost != null) throw new IllegalStateException("Already attached");
    ...
    // Set up the OnBackPressedCallback
    if (host instanceof OnBackPressedDispatcherOwner) {
        OnBackPressedDispatcherOwner dispatcherOwner = ((OnBackPressedDispatcherOwner) host);
        mOnBackPressedDispatcher = dispatcherOwner.getOnBackPressedDispatcher();
        LifecycleOwner owner = parent != null ? parent : dispatcherOwner;
        mOnBackPressedDispatcher.addCallback(owner, mOnBackPressedCallback);
    }
    ...
}

//3.5.3 when executing a transaction, try to modify the callback object status
void scheduleCommit() {
     ...
    updateOnBackPressedCallbackEnabled();
}

private void updateOnBackPressedCallbackEnabled() {
    if (mPendingActions != null && !mPendingActions.isEmpty()) {
        mOnBackPressedCallback.setEnabled(true);
        return;
    }

    mOnBackPressedCallback.setEnabled(getBackStackEntryCount() > 0 && isPrimaryNavigation(mParent));
}

//3.5.4 recycling
public void dispatchDestroy() {
    mDestroyed = true;
    ...
    if (mOnBackPressedDispatcher != null) {
        // mOnBackPressedDispatcher can hold a reference to the host
        // so we need to null it out to prevent memory leaks
        mOnBackPressedCallback.remove();
        mOnBackPressedDispatcher = null;
    }
}

If you lack a clear concept of fragment transaction, be sure to read an article I wrote earlier:Do you really understand fragment? Core principle analysis of androidx fragment

After discussing the usage & implementation principle of onbackpresseddispatcher, let’s practice it directly through some application scenarios:


4. Press the back key again to exit

Pressing the return key again to exit is a very common function, which is essentially an exit recovery. There are also many incomplete implementation methods on the Internet. In fact, this function seems simple, but it hides some optimization details. Let’s have a look~

4.1 demand analysis

First, I analyzed dozens of well-known apps and summarized four types of return key interaction:

classification describe give an example
1. System default behavior Return key events are handled by the system, and the application does not intervene Wechat, Alipay, etc
2. Press again to exit Whether to click the back button again within two seconds. If yes, exit Iqiyi, Gaode, etc
3. Return to Home tab Press once to return to the Home tab, and then press once to exit Facebook, instagram, etc
4. Refresh information flow Press once to refresh the information flow, and then press once to exit Little red book, today’s headlines, etc
Android | jetpack's new gesture for handling fallback events -- onbackpresseddispatcher

4.2 how to exit the app?

Interaction logic mainly depends on the product form and specific application scenarios. For our technical students, we also need to consider the differences of different ways to exit the app. By observing the actual effects of the above apps, I sorted out the following four ways to exit the app:

  • 1. System default behavior:Submit the fallback event to the system for processing, and the default behavior of the system is finish() current activity. If the current activity is at the bottom of the stack, transfer the activity task stack to the background;

  • 2. Call movetasktoback():Manually transfer the task stack of the current activity to the background, and the effect is similar to the default behavior of the system (this method receives a nonroot parameter: true: only the current activity is required to be at the bottom of the stack, false: the current activity is not required to be at the bottom of the stack). Because the activity is not actually destroyed, the next time the user returns to the application, it will be hot started;

  • 3. Call finish():End the current activity. If the current activity is at the bottom of the stack, the activity task stack will be destroyed. If the current activity is the last component of the process, the process will also end. It should be noted that the memory will not be recovered immediately after the process is completed. In the future (for a period of time), the user will restart the application as a warm start, which is faster than a cold start;

  • 4. Call system Exit (0) kill appKill the process JVM, and it will take more time for the user to restart as a cold start in the future.

So, how should we choose? In general, “calling movetasktoback()” performs best. There are two arguments:

  • 1. The purpose of clicking the back button twice is to save the user and confirm that the user really needs to exit. Then, the behavior after exiting is the same as the default behavior without interception. This point can be satisfied by movetasktoback(), while finish() and system The behavior of exit (0) is more serious than the default behavior.

  • 2. Movetasktoback() quits the application and does not really destroy the application. When the user returns to the application again, it is a hot start and the recovery speed is the fastest.

It should be noted that system. Is generally not recommended Exit (0) and process Killprocess (process. Mypid) to exit the application. Because the performance of these APIs is not ideal:

  • 1. When the called activity is not at the top of the stack, kill the process and the system will restart the app immediately (it may be that the system thinks that the foreground app is terminated unexpectedly and will restart automatically);

  • 2. When the app exits, the sticky service will automatically restart (service #onstartcommand() returns to start_ Sticky service), sticky service will run consistently unless stopped manually.

classification Apply return effect give an example
1. System default behavior Hot start Wechat, Alipay, etc
2. Call movetasktoback() Hot start QQ music, little red book, etc
3. Call finish() Warm start To be confirmed (alternative iqiyi, Gaode, etc.)
4. Call system Exit (0) kill app cold boot To be confirmed (alternative iqiyi, Gaode, etc.)

Process. Killprocess (process. Mypid) and system What is the difference between exit (0)? todo

4.3 specific code implementation

BackPressActivity.kt

fun Context.startBackPressActivity() {
    startActivity(Intent(this, BackPressActivity::class.java))
}

class BackPressActivity : AppCompatActivity(R.layout.activity_backpress) {

    //Viewbinding + kotlin delegation
    private val binding by viewBinding(ActivityBackpressBinding::bind)

    /**
     *The last time the return key was clicked
     */
    private var lastBackPressTime = -1L

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //Add callback object
        onBackPressedDispatcher.addCallback(this, onBackPress)

        //Return button
        binding.ivBack.setOnClickListener {
            onBackPressed()
        }
    }

    private val onBackPress = object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            if (popBackStack()) {
                return
            }
            val currentTIme = System.currentTimeMillis()
            if (lastBackPressTime == -1L || currentTIme - lastBackPressTime >= 2000) {
                //Display prompt information
                showBackPressTip()
                //Record time
                lastBackPressTime = currentTIme
            } else {
                //Exit application
                finish()
                // android.os.Process.killProcess(android.os.Process.myPid())
                // System.exit(0) // exitProcess(0)
                // moveTaskToBack(false)
            }
        }
    }

    private fun showBackPressTip() {
        Toast. Maketext (this, "press again to exit", toast. Length_short) show();
    }
}

The logic of this code is not complicated. We mainly added a callback object through onbackpresseddispatcher#addcallback(), thus interfering with the logic of the return key event: “click the return key for the first time to pop up a prompt, and click the return key again within two seconds to exit the application”.

In addition, the following code needs to be explained:private val binding by viewBinding(ActivityBackpressBinding::bind)。 In fact, this is a view binding scheme using viewbinding + kotlin delegate attribute. Compared with the traditional schemes such as findviewbyid, butterknife and kotlin synthetics, this scheme performs better from multiple angles. For specific analysis, you can see an article I wrote before:Android | viewbinding and kotlin entrust a combination of two swords

4.4 Optimization: compatible with fragment return stack

The previous section can basically meet the requirements, but consider a case: there are multiple fragment transactions added to the return stack in the page. When you click the return key, you need to clear the return stack in turn, and finally follow the logic of “press the return key again to exit”.

At this point, you will find that the method in the previous section will not directly exit the logic after the stack is cleared. The reason is also well understood. Because the joining time of the fallback object of the activity is earlier than that of the fallback object in the fragmentmanagerimpl, the fallback logic of the activity takes priority. The solution is to pop up the fragment transaction return stack manually in the activtiy fallback logic. The complete demonstration code is as follows:

BackPressActivity.kt

class BackPressActivity : AppCompatActivity(R.layout.activity_backpress) {

    private val binding by viewBinding(ActivityBackpressBinding::bind)

    /**
     *The last time the return key was clicked
     */
    private var lastBackPressTime = -1L

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        addFragmentToStack()
        onBackPressedDispatcher.addCallback(this, onBackPress)

        binding.ivBack.setOnClickListener {
            onBackPressed()
        }
    }

    private fun addFragmentToStack() {
        //Tip: in order to focus on the problem, the scene reconstructed by activity is not considered here
        for (index in 1..5) {
            supportFragmentManager.beginTransaction().let { it ->
                it.add(
                    R.id.container,
                    BackPressFragment().also { it.text = "fragment_$index" },
                    "fragment_$index"
                )
                it.addToBackStack(null)
                it.commit()
            }
        }
    }

    /**
     *@ return true: no fragment pop-up false: fragment pop-up
     */
    private fun popBackStack(): Boolean {
        //When the fragment state is saved, the return stack will not pop up
        return supportFragmentManager.isStateSaved
                || supportFragmentManager.popBackStackImmediate()
    }

    private val onBackPress = object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            if (popBackStack()) {
                return
            }
            val currentTIme = System.currentTimeMillis()
            if (lastBackPressTime == -1L || currentTIme - lastBackPressTime >= 2000) {
                //Display prompt information
                showBackPressTip()
                //Record time
                lastBackPressTime = currentTIme
            } else {
                //Exit application
                finish()
                // android.os.Process.killProcess(android.os.Process.myPid())
                // System.exit(0) // exitProcess(0)
                // moveTaskToBack(false)
            }
        }
    }

    private fun showBackPressTip() {
        Toast. Maketext (this, "press again to exit", toast. Length_short) show();
    }
}

4.5 use in fragment

TestFragment.kt

class TestFragment : Fragment() {
    private val dispatcher by lazy {requireActivity().onBackPressedDispatcher}
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        dispatcher.addCallback(this, object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                Toast.makeText(context, "TestFragment - handleOnBackPressed", Toast.LENGTH_SHORT).show()
            }
        })
    }
}

4.6 other finish() methods

In addition, finish () has some similar APIs, which can be supplemented as follows:

  • Finishaffinity(): close all activities under the current activity in the current activity task stack (for example, a starts B and B starts C. If b calls finishaffinity(), a and B will be closed while C remains). This API is introduced after API 16, preferably through activitycompat Finishaffinity() call.
  • Finishaftertransition(): after the transition animation is executed, the transition animation needs to be defined through activityoptions. This API is introduced after API 21, preferably through activitycompat Finishaftertransition() call.

5. Summary

That’s all for the discussion on onbackpresseddispatcher. I’ll leave you two questions to think about:

  • 1. If a dialog pops up on the activity, click the return key to close the dialog first, or will it be distributed to onbackpresseddispatcher? What if popupwindow pops up?
  • 2. A floating layer pops up in the WebView of activity. How to click the back button to close the floating layer first and click again to return to the page?

reference material


It’s not easy to create. Your “three company” is the biggest driving force of ugliness. I’ll see you next time!

Android | jetpack's new gesture for handling fallback events -- onbackpresseddispatcher

Recommended Today

Mutually exclusive locks commonly used by Swift

@Synchronized is the thread mutex method in OC. Swift corresponds to objc_ sync_ Enter (self) and objc_ sync_ exit(self)。The parameters in the method can only make self, and other parameters cannot achieve the purpose of mutual exclusion. @objc private func remove() { print(“\(Thread.current)::\(array.count)”) //Mutex objc_sync_enter(self) while array.count > 0 { array.removeLast() print(“\(Thread.current)::\(array.count)”) } print(“\(Thread.current)::\(array.count)”) objc_sync_exit(self) […]