Like attention, no more lost, your support means a lot to me!

🔥 Hi, I’m Chouchou. GitHub · Android-Notebook has been included in this article. Welcome to grow up with Chouchou Peng. (Contact information at GitHub)


preface

  • Since Androidx.Activity 1.0.0, Google has introduced the OnBackPressedDispatcher API to handle fallback events, aiming to optimize fallback event handling: You can define the fallback logic anywhere, instead of relying on Activity#onBackPressed();
  • In this article, I will introduce the use of OnBackPressedDispatcher & implementation principle & application scenarios. Please be sure to like and follow if you can help, it really means a lot to me.
  • The code for this article can be downloaded from DemoHall·HelloAndroidX.

directory


Front knowledge

The content of this article will involve the following pre/related knowledge, dear I have prepared for you, please enjoy ~

  • Do you really know Fragment? AndroidX Fragment core principle analysis

1. An overview of the

  • OnBackPressedDispatcher fixes the problem: The Activity can be handled with the onBackPressed() callback method, but the Fragment/View has no direct callback method. Now we can use OnBackPressedDispatcher instead of Activity#onBackPressed() to implement the fallback logic more elegantly.

  • The overall processing flow of OnBackPressedDispatcher: The dispatcher adopts the responsibility chain design mode, and the callback object added to the dispatcher will become a node in the responsibility chain. When the user fires the back key, the chain of responsibilities is traversed sequentially, and if the callback object is Enabled, the fallback event is consumed and the traversal stops. If the last event is not consumed, it goes back to Activity#onBackPressed().

  • OnBackPressedDispatcher vs. other schemes: Before OnBackPressedDispatcher, we could only handle the fallback event through “trick” methods:

    • 1. Define callback methods in your Fragment, passing callback events from Activity#onBackPressed() (bad: increased Activity/Fragment coupling);
    • 2, set key listener setOnKeyListener in Fragment root layout (disadvantages: inflexibly & multiple fragments listening conflict).

2. What apis does OnBackPressedDispatcher have?

There are several main ones, but the other apis are easier to understand. LifecycleOwner addCallback(LifecycleOwner, callback) will not join the distribution chain until LifecycleOwner LifecycleOwner enters the LifecycleOwner State. LifecycleOwner is removed from the distribution responsibility chain when Lifecycle.state. STOP is entered.

Public void addCallback(OnBackPressedCallback OnBackPressedCallback) Public void addCallback(LifecycleOwner owner, OnBackPressedCallback OnBackPressedCallback) 3. Check whether the callback is enabled public Boolean hasEnabledCallbacks( Public OnBackPressedDispatcher(@nullable Runnable fallbackOnBackPressed) { mFallbackOnBackPressed = fallbackOnBackPressed; }Copy the code

3. OnBackPressedDispatcher source code analysis

OnBackPressedDispatcher source code is not much, I directly with the problem to help you comb through the internal implementation principle of OnBackPressedDispatcher:

3.1 How does an Activity send events to OnBackPressedDispatcher?

A: ComponentActivity incorporates the dispatcher object internally, and the return key callback onBackPressed() is directly dispatched to OnBackPressedDispatcher#onBackPressed(). In addition, the Activity’s own fallback logic is encapsulated as Runnable and handed over to the dispenser for processing.

androidx.activity.ComponentActivity.java

private final OnBackPressedDispatcher mOnBackPressedDispatcher = new OnBackPressedDispatcher(new Runnable() { @Override Public void the run () {/ / the Activity itself back logic ComponentActivity. Super. OnBackPressed (); }}); @Override @MainThread public void onBackPressed() { mOnBackPressedDispatcher.onBackPressed(); } @NonNull @Override public final OnBackPressedDispatcher getOnBackPressedDispatcher() { return mOnBackPressedDispatcher; }Copy the code

3.2 What is the processing flow of OnBackPressedDispatcher?

A: The distributor adopts the chain of responsibility design as a whole, and every callback object added to the distributor becomes a node in the chain of responsibility. When the user fires the back key, the chain of responsibilities is traversed sequentially, and if the callback object is Enabled, the fallback event is consumed and the traversal stops. If the last event is not consumed, it goes back to Activity#onBackPressed().

OnBackPressedDispatcher.java

// Final callback: Activity#onBackPressed() @private final Runnable mFallbackOnBackPressed; Final ArrayDeque<OnBackPressedCallback> mOnBackPressedCallbacks = new ArrayDeque<>(); Public OnBackPressedDispatcher() {this(null); } constructor public OnBackPressedDispatcher(@nullable Runnable fallbackOnBackPressed) {mFallbackOnBackPressed = fallbackOnBackPressed; } @mainThread public Boolean hasEnabledCallbacks() {Iterator<OnBackPressedCallback> Iterator = mOnBackPressedCallbacks.descendingIterator(); while (iterator.hasNext()) { if (iterator.next().isEnabled()) { return true; } } return false; } entry methods: Each callback method on the responsibility chain can only be called if the previous callback is unEnabled. If if neither is enabled, MFallbackOnBackPressed @mainThread public void onBackPressed() {Iterator<OnBackPressedCallback> Iterator = mOnBackPressedCallbacks.descendingIterator(); while (iterator.hasNext()) { OnBackPressedCallback callback = iterator.next(); if (callback.isEnabled()) { callback.handleOnBackPressed(); / / return of consumption; } } if (mFallbackOnBackPressed ! = null) { mFallbackOnBackPressed.run(); }}Copy the code

3.3 Is the callback performed on the main thread or child thread?

A: The main thread, the distributor’s entry method Activity#onBackPressed() is executed on the main thread, so is the callback method. In addition, the addCallback() method that adds callbacks also requires execution on the main thread, using a non-concurrent safe container ArrayDeque inside the dispenser to store callback objects.

3.4 Can OnBackPressedCallback be added to different dispensers simultaneously?

Answer: Yes.

3.5 How to Rollback a Fragment Transaction added to the Return Stack?

A: The FragmentManager also rolls back transactions to OnBackPressedDispatcher for processing. First, when Fragment Attach is attached, a callback object is created and added to the dispenser, which pops back to the top of the stack when the callback is processed. However, the initial state is not enabled, and the callback object is changed to enabled only after the transaction is added to the return stack. The source code is as follows:

FragmentManagerImpl.java

// 3.5.1 Dispatcher and callback object (initially disabled) private OnBackPressedDispatcher mOnBackPressedDispatcher; private final OnBackPressedCallback mOnBackPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { execPendingActions(); if (mOnBackPressedCallback.isEnabled()) { popBackStackImmediate(); } else { mOnBackPressedDispatcher.onBackPressed(); }}}; 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 Attempting to change the state of the callback object while executing a transaction void scheduleCommit() {... updateOnBackPressedCallbackEnabled(); } private void updateOnBackPressedCallbackEnabled() { if (mPendingActions ! = null && ! mPendingActions.isEmpty()) { mOnBackPressedCallback.setEnabled(true); return; } mOnBackPressedCallback.setEnabled(getBackStackEntryCount() > 0 && isPrimaryNavigation(mParent)); } 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; }}Copy the code

If you don’t have a clear idea of Fragment transactions, take a look at an article I wrote earlier: Do you really understand Fragments? AndroidX Fragment core principle analysis

After discussing the use method & implementation principle of OnBackPressedDispatcher, let’s directly practice through some application scenarios:


4. Press the Back key again to exit

Pressing the back key again to exit is a very common feature, essentially an exit retrieval. There are also many incomplete implementations circulating online. In fact, this function seems simple, but hidden some optimization details, let’s take a look ~

4.1 Requirement Analysis

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

classification describe For example,
1. Default system behavior The return key event is handed over to the system without application intervention Wechat, Alipay, etc
2. Press exit again Whether to click the back button again within two seconds, if yes, exit Iqiyi, Autonavi, etc
3. Return to Tab Press once to return to the home Tab, and press again to exit Facebook, sets, etc
4. Refresh the information flow Press once to refresh the information flow and press again to exit Little red book, today’s headlines, etc

4.2 How Do I Exit the App?

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

  • Finish () The current Activity. If the current Activity is at the bottom of the stack, move the Activity to the background.

  • 2. Call moveTaskToBack() to manually push the current Activity into the background, similar to the system’s default behavior. (This method takes a nonRoot argument: true: requires that only the current Activity is valid at the bottom of the stack. The current Activity is not required to be at the bottom of the stack. Because the Activity is not actually destroyed, it is hot started the next time the user returns to the application;

  • Call Finish () to end the current Activity. If the current Activity is at the bottom of the stack, destroy the Activity stack. If the current Activity is the last component of the process, the process also ends. Note that the memory will not be reclaimed immediately after the process ends. In the future (for a period of time), the application will be restarted at a warm start speed, which is faster than that at a cold start.

  • 4. Call System.exit(0) to kill the application. Kill the process JVM.

So, how should we choose? In general, “Call moveTaskToBack()” performs best, two arguments:

  • 1. The purpose of clicking the back button twice is to retrieve the user and confirm that the user really needs to exit. Then, the exit behavior is the same as the default behavior without interception, which can be satisfied by moveTaskToBack(), while finish() and System.exit(0) behave worse than the default behavior;

  • 2. MoveTaskToBack () does not actually destroy the application, and the user returns to the application with a hot start and the fastest recovery.

Note that system.exit (0) and process.killProcess (process.mypid) are generally not recommended to exit the application. Because these apis don’t perform well:

  • 1. When the Activity is not at the top of the stack, the system will immediately restart the App after killing the process (possibly because the system thinks the foreground App was accidentally terminated and will automatically restart);

  • 2. When the App exits, sticky services will automatically restart (Service#onStartCommand() returns a START_STICKY Service) and run consistently unless manually stopped.

classification Apply return effect For example,
1. Default system behavior Warm start Wechat, Alipay, etc
2. Call moveTaskToBack() Warm start QQ music, xiaohongshu and so on
3. Call Finish () Wen started To be confirmed (iQiyi, Autonavi, etc.)
4. Call System.exit(0) to kill the application Cold start To be confirmed (iQiyi, Autonavi, etc.)

What is the difference between process.killProcess (process.mypid) and system.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 delegate private val binding by ViewBinding (ActivityBackpressBinding: : bind) / * * * * last click on the return key time/private var lastBackPressTime = 1 l override fun onCreate(savedInstanceState: Bundle?) {super. OnCreate (savedInstanceState) / / add a callback object onBackPressedDispatcher. AddCallback (this, Binding onBackPress) / / return button. IvBack. SetOnClickListener {onBackPressed ()}} private val onBackPress = object: OnBackPressedCallback(true) { override fun handleOnBackPressed() { if (popBackStack()) { return } val currentTIme = System.currenttimemillis () if (lastBackPressTime = = 1 l | | currentTIme - lastBackPressTime > = 2000) {/ / show the message 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, "toast.length_short ", toast.length_short).show(); }}Copy the code

The logic of this code is not complicated, we mainly interfere with the logic of the return key event by adding a callback object via OnBackPressedDispatcher#addCallback() : “Click the return key for the first time and it will prompt you, click the return key again within two seconds to exit the application”.

In addition, the need to explain this sentence code: private val binding by viewBinding ActivityBackpressBinding: : bind (). In fact, this is a ViewBinding scheme that uses the ViewBinding + Kotlin delegate property, and actually performs better in many ways than conventional approaches like findViewById, ButterKnife, and Kotlin synwide-footing. Concrete analysis you can see before I wrote an article: Android | ViewBinding and Kotlin entrust shuangjian combination

4.4 Optimization: Compatible Fragment return stack

The previous section basically meets the requirements, but consider a case where there are multiple Fragment transactions on the page and they are added to the return stack. When you click the return key, you need to clear the return stack successively, and then go to the logic of “press the return key again to exit”.

At this point, you’ll notice that the method in the previous section does not wait for the return stack to clear before going to the exit logic. The reason is easy to understand, because the Activity’s rollback object is added earlier than the FragmentManagerImpl’s rollback object, so the Activity’s rollback logic takes precedence. The solution is to manually pop the Fragment transaction return stack in the Activtiy rollback logic. The complete demo code is as follows:

BackPressActivity.kt

class BackPressActivity : AppCompatActivity(R.layout.activity_backpress) { private val binding by viewBinding(ActivityBackpressBinding::bind) /** 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: To focus on the problem, the Activity reconstruction scenario 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.mit ()}}} /** * @return true: No Fragment is displayed False: No Fragment is displayed */ private fun popBackStack(): Boolean {// When the Fragment state is saved, Don't pop up return stack return supportFragmentManager. IsStateSaved | | supportFragmentManager. PopBackStackImmediate ()} private val onBackPress = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { if (popBackStack()) { return } val currentTIme = System.currenttimemillis () if (lastBackPressTime = = 1 l | | currentTIme - lastBackPressTime > = 2000) {/ / show the message 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, "toast.length_short ", toast.length_short).show(); }}Copy the code

4.5 Used in Fragments

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()
            }
        })
    }
}
Copy the code

4.6 Other Finish () methods

Finish () also has some similar apis to add to the list:

  • FinishAffinity () : Closes all activities in the current Activity stack that are under the current Activity (for example, A starts B, B starts C; if B calls finishAffinity(), A and B are closed, while C remains). The API in API 16 introduced after, the best by ActivityCompat. FinishAffinity () call.
  • FinishAfterTransition () : Finish the Activity after executing the transition animation. You need to define the transition animation through ActivityOptions. The API introduced in API after 21, through best ActivityCompat. FinishAfterTransition () call.

5. To summarize

So much for the onBackpress Dispatcher discussion, and I leave you with two questions to ponder:

  • 1. If a Dialog is displayed on your Activity, pressing the return key closes the Dialog or will it be sent to OnBackPressedDispatcher? What if I pop up a PopupWindow?
  • 2. There is a floating layer in the WebView of the Activity. How to close the floating layer by clicking the back button, and then clicking again to go back to the page?

The resources

  • Jetpack Application Architecture Guide – Official documentation
  • Provides custom return navigation – official documentation
  • Fragment past, Present, and Future — Google Developers

Creation is not easy, your “three lian” is chouchou’s biggest motivation, we will see you next time!