Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Show confirmation on back/up in Fragment with Navigation Architecture Component

I am using the Navigation Architecture Component for Android.

For one of my fragments I wish to intercept "back" and "up" navigation, so that I can show a confirmation dialog before discarding any unsaved changes by the user. (Same behavior as the default Calendar app when you press back/up after editing event details)

My current approach (untested) is as follows:

For "up" navigation, I override onOptionsItemSelected on the fragment:

override fun onOptionsItemSelected(item: MenuItem?): Boolean {
    if(item?.itemId == android.R.id.home) {
        if(unsavedChangesExist()) {
            // TODO: show confirmation dialog
            return true
        }
    }
    return super.onOptionsItemSelected(item)
}

For "back" navigation, I created a custom interface and callback system between the fragment and its activity:

interface BackHandler {
    fun onBackPressed(): Boolean
}

class MainActivity : AppCompatActivity() {
    ...

    val backHandlers: MutableSet<BackHandler> = mutableSetOf()

    override fun onBackPressed() {
        for(handler in backHandlers) {
            if(handler.onBackPressed()) {
                return
            }
        }
        super.onBackPressed()
    }

    ...
}

class MyFragment: Fragment(), BackHandler {
    ...

    override fun onAttach(context: Context) {
        super.onAttach(context)
        if (context is MainActivity) {
            context.backHandlers.add(this)
        }
    }

    override fun onDetach() {
        (activity as? MainActivity)?.backHandlers?.remove(this)
        super.onDetach()
    }

    override fun onBackPressed(): Boolean {
        if(unsavedChangedExist()) {
            // TODO: show confirmation dialog
            return true
        }
    }

    ...
}

This is all pretty gross and boilerplatey for such a simple thing. Is there a better way?

like image 958
James Avatar asked Dec 13 '18 11:12

James


People also ask

What is popUpTo and popUpToInclusive?

When navigating back to destination A, we also popUpTo A, which means that we remove B and C from the stack while navigating. With app:popUpToInclusive="true" , we also pop that first A off of the stack, effectively clearing it.

How can we prevent fragment recreation in navigation component?

Just add the pop inclusive to all your action in nav graph. What the above pop behavior will do is, when you are navigating from, say C > B, it will pop everything till B (inclusive) from the back stack and add the latest instance of B on the back stack.


2 Answers

As of androidx.appcompat:appcompat:1.1.0-beta01, in order to intercept the back button with navigation component you need to add a callback to the OnBackPressedDispatcher. This callback has to extend OnBackPressedCallback and override handleOnBackPressed. OnBackPressedDispatcher follows a chain of responsibility pattern to handle the callbacks. In other words, if you set your callback as enabled, only your callback will be executed. Otherwise, OnBackPressedDispatcher will ignore it and proceed to the next callback, and so on until it finds an enabled one (this might be useful when you have more than one callback, for instance). More info on this here.

So, in order to show your dialog, you would have to do something similar to this:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  super.onViewCreated(view, savedInstanceState)

  val callback = object : OnBackPressedCallback(true /** true means that the callback is enabled */) {
    override fun handleOnBackPressed() {
        // Show your dialog and handle navigation
    }
  }

  // note that you could enable/disable the callback here as well by setting callback.isEnabled = true/false

  requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
}

As for the up button, it seems like (at least for now) there aren't many possibilities. The only option I could find up until now that uses the navigation component is to add a listener for the navigation itself, which would handle both buttons at the same time:

navController.addOnDestinationChangedListener { navController, destination ->
  if (destination.id == R.id.destination) {
    // do your thing
  }
}

Regardless, this has the caveat of allowing the activity or fragment where you add the listener to know about destinations it might not be supposed to.

like image 180
Ricardo Costeira Avatar answered Oct 21 '22 22:10

Ricardo Costeira


With the navigation architecture components, you can do something like this:

  1. Tell your activity to dispatch all up clicks on the home button(back arrow) to anyone listening for it. This goes in your activity.
override fun onOptionsItemSelected(item: MenuItem): Boolean {
     if (item.itemId == android.R.id.home) {
         onBackPressedDispatcher.onBackPressed()
         return true
     }
     return super.onOptionsItemSelected(item)
}
  1. Then in your fragments, consume the events like so
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        requireActivity().onBackPressedDispatcher.addCallback(this) {
           if (*condition for showing dialog here*) {
               // Show dialog
           } else {
               // pop fragment by calling function below. Analogous to when the user presses the system UP button when the associated navigation host has focus.
               findNavController().navigateUp()
           }
        }
    }
like image 29
11m0 Avatar answered Oct 21 '22 21:10

11m0