A Reasonable Way for Android Fragment to Listen for the Return Key

  • 2021-12-09 10:06:08
  • OfStack

Opening

The following scenarios are fabricated:

Product Manager: "Ronaldinho, in this information sending interface, if the user enters the content, when clicking the return button, ask the user whether to save the draft box first.".

Ronaldinho: "Roger that. This question is simple."

After that, Ronaldinho was ready to deal with it, but then found that the information editing interface was an Fragment, but Fragment did not provide direct processing of returning key clicks; Although Ronaldinho is a dish, he has been fishing for some years. This problem can't beat Ronaldinho.

Ronaldinho thought, anyway, Activity provides onBackPressed method, and it is good to distribute this operation to Fragment in the worst case, but for Virgo Ronaldinho, on the basis of solving the problem, at least the code should be written beautifully at 1 point, and if it is written beautifully at 1 point, it will be comfortable at 1 point, and it will be comfortable at 1 point... (the content here is very long).

Ronaldinho firmly believes that "all roads lead to Rome", and we have to go not only to Rome, but also to the scenery. Therefore, Ronaldinho decided to work hard on "how Fragment listens to the click of the return button";

Why is the focus on Fragment listening for the return key, not others? In fact, in the current development process, the proportion of Fragment is very large. For individuals, the interface implementation of almost the whole project is based on Fragment instead of Activity.

1. The most lowB method (not recommended)

This is the preparatory plan in Ronaldinho's heart. When there is really no way, this method will be adopted, that is, as mentioned earlier, we can distribute it to Fragment when Activity executes onBackPressed; So what do we use to distribute it? This distribution is just like a link between Activity and Fragment. It is enough for both parties to access this object, so one of the available options is to use ViewModel. Of course, there are other options, so I won't talk about them in detail here.

2. Use OnKeyListener (not recommended)

This method may not be commonly used, and it is not easy to think of this aspect, so this method is not recommended, and it is simply understood;

By setting OnKeyListener of View to listen for the processing of the return key, this method has no big drawbacks, just pay attention to the following two points:

1. If this function is encapsulated in Fragment base class, there may be an override problem; For example, if OnKeyListener is set in the base class and OnKeyListener is set in the subclass, the set listening will replace the default listening, which leads to unexpected possibilities, but this problem is almost unlikely to happen.

2. Note that this method will change the order of return key processing, that is, the callback of OnKeyListener will be processed first, and then the onBackPressed of Activity will be processed, so pay attention to this relationship.

3. Modalities provided by Jetpack

In fact, the distribution of return key has been officially supported. In Activity, an object for distributing return key events is provided. This object is obtained by calling getOnBackPressedDispatcher () method of Activity, because this object is provided in androidx. activity. ComponentActivity at the lower level (AppCompatActivity- > FragmentAcitivty- > androidx. activity. ComponentActivity), so you can get this object directly in Fragment to add callbacks;

Official information portal


// Official usage example  
public class FormEntryFragment extends Fragment {
 @Override
 public void onAttach(@NonNull Context context) {
  super.onAttach(context);
  // Define callbacks 
  OnBackPressedCallback callback = new OnBackPressedCallback(
   true // default to enabled
  ) {
   @Override
   public void handleOnBackPressed() {
    showAreYouSureDialog();
   }
  };
  
  // Get Activity Add a callback to the return key distributor of 
  requireActivity().getOnBackPressedDispatcher().addCallback(
   this, // LifecycleOwner
   callback);
 }
}

Simple and clear, this matter seems to be over ~ ~

However, with in-depth understanding, things seem to be not so simple. After source code analysis and data collection, it is found that if used directly, there will be the following drawbacks:

1. Unable to pass up during Fragment callback processing

2. Whether the callback is available requires active marking, not runtime determination

Simply talk about the flow of OnBackPressedDispatcher distributing return keys:


	// Official source code 
 @MainThread
 public void onBackPressed() {
  Iterator<OnBackPressedCallback> iterator =
    mOnBackPressedCallbacks.descendingIterator();
  while (iterator.hasNext()) {
   OnBackPressedCallback callback = iterator.next();
   if (callback.isEnabled()) {
    callback.handleOnBackPressed();
    return;
   }
  }
  if (mFallbackOnBackPressed != null) {
   mFallbackOnBackPressed.run();
  }
 }

When the return key event is distributed, the registered callbacks will be iterated in reverse order. If the callback isEnabled is set to true, the callback method will be executed and the distribution will end;

How did the aforementioned drawbacks come into being? If an Activity has two Fragment, A and B, All of them have registered the return key click event (some children's shoes will say that this kind of scene is unlikely to exist. Indeed, there are not many such scenes, but it doesn't mean there are no, and it's not a bad thing to do some understanding). And the isEnabled of both callbacks is set to true, so when the event is distributed, the event will be distributed to B, but B does not need to deal with the return key event at this time, but B has no way to continue to pass the event to A;

"You are stupid, you set isEnable to false without executing the return key event in B."

"Yes, B does not execute the event so it should be set to false, but how do I know when to set it to false? Do you dynamically bind the value of the judgment condition to set it?"

Turn 1 and think, "Hey, it seems that you can dynamically modify the callback isEnabled value. Can't you bind the callback value with an LiveData?"
That's the reason, but I don't want to do extra work. I don't want to do this. Who knows how complicated the dynamic judgment condition is? Can't I judge when the return button clicks?

4. Lingji 1, official upgraded version (recommended method)

Isn't there the above two drawbacks in the official way? It's good to solve these two problems; Therefore, combining the advantages of the official OnBackPressedDispatcher and OnKeyListener, andme. arch. activity. AMBackPressedDispatcher was created. While retaining the original official functions, the event distribution process was changed, and the return key holder 1 was passed in to solve some more complex needs;


 @MainThread
 fun onBackPressed(): Boolean {
  if (!hasRegisteredCallbacks())
   return false

  val iterator = mOnBackPressedCallbacks.descendingIterator()
  while (iterator.hasNext()) {
   val callback = iterator.next()
   // Determine whether the callback needs to consume events before deciding whether to continue passing 
   if (callback.handleOnBackPressed(owner)) {
    return true
   }
  }
  return false
 }

5. Official Use Tips Edition

This method is actually a kind of idea provided by my group friends after publishing the article. To be honest, it is very skillful. When I first saw it, my eyes lit up; Its core principle is that callbacks registered by default are available, In the callback execution, first judge whether you need to execute the callback, if you don't need to execute the callback, then set your own isEnabled to false, and then call OnBackPressedDispatcher to redistribute the return key event (because you have set yourself to false at this time, you won't respond to the callback at this time). After calling the method, you will set isEnabled to true, which is recursive. This way is good;

At the beginning, the code provided by group friends has 1 lost defect. The following is the revised code. These two methods are defined in Fragment. When you need to bind the return key to monitor, you can call one of the two methods (it is recommended to call the method related to the life cycle);


fun addOnBackPressed(onBackPressed: () -> Boolean): OnBackPressedCallback {
  val callback = object : OnBackPressedCallback(true) {
   override fun handleOnBackPressed() {
    if (!onBackPressed()) {
     isEnabled = false
     requireActivity().onBackPressedDispatcher.onBackPressed()
     isEnabled = true
    }
   }
  }
  requireActivity().onBackPressedDispatcher.addCallback(callback)
  return callback
 }

 fun addOnBackPressed(owner: LifecycleOwner, onBackPressed: () -> Boolean): OnBackPressedCallback {
  val callback = object : OnBackPressedCallback(true) {
   override fun handleOnBackPressed() {
    if (!onBackPressed()) {
     isEnabled = false
     requireActivity().onBackPressedDispatcher.onBackPressed()
     isEnabled = true
    }
   }
  }
  requireActivity().onBackPressedDispatcher.addCallback(owner,callback)
  return callback
 }

However, after careful consideration, I didn't use this method in the end. Although this method is no problem in almost 8910% of cases, I think there may still be scenes that can't be satisfied;

For example, one Fragment is added to one Activity, and two ChildFragment, A and B, are added to this Fragment in sequence. Do you want to return to A or finish when B performs return processing? Or something else, that is to say, we can't determine whether we need to call the Activity. super. onBackPressed method directly when Fragment performs return key processing.

We can never predict how complex the user's scenario is and how abnormal the requirements are, so consider it as much as possible.

Summarize

To sum up, I will continue to use the fourth scheme I wrote, and the fifth scheme is also recommended, after all, there is no problem in most scenarios
So let's consider whether the fourth option is feasible or not.

1. Functionality

The functional requirements are met, and at least for now, there are no scenarios where problems may occur

2. Invasive

It has almost no effect on the user scene, but only provides a visible method for the user to handle the return key event

3. Replaceability

If the fourth scheme is adopted, is it easy to replace it with the fifth scheme? 1. It's just a two-sentence code thing

Or is it easy to change to another scheme? It is also a matter of 1 or 2 sentences of code

And even if it is replaced by other schemes, it will not cause any impact on the existing system, because for the demand of Fragment listening to the return key, the core of this demand is to need a method to deal with the return key event in Fragment, and other things are insensitive to users
Therefore, there is nothing wrong with the overall feeling;

If you have better ideas, welcome to communicate, thank you very much;

In addition, the above functions do not only support handling return key events in Fragment. Theoretically, anyone who wants to listen to return key processing can get AMBackPressedDispatcher objects through Activity and add callbacks.

Andme Github Address


Related articles: