How to provide the custom back navigation handling behavior on Android 12 and higher.

Lukoh Nam
5 min readDec 15, 2022

--

Photo by Raul Taciu on Unsplash

Android maintains a back stack of destinations as the user navigates throughout your application. This will usually allow Android to properly navigate to the previous destination when pressing the back button. However, there are a few cases where apps should implement their own back behavior to provide the best user experience.

Here is how to provide handling the custom back press.

Implement custom back navigation

You can use the OnBackPressedDispatcher (obtained by calling getOnBackPressedDispatcher()) to control the behavior of the back button. OnBackPressedDispatcher controls how back button events are dispatched to one or more OnBackPressedCallback objects. Only when the callback is enabled (i.e. isEnabled() returns true) the dispatcher handles the back button event by calling the callback’s handleOnBackPressed().
Create a new OnBackPressedDispatcher that dispatches System back button pressed events to one or more OnBackPressedCallback instances.
Add a new OnBackPressedCallback. Callbacks are invoked in the reverse order in which they are added, so this newly added OnBackPressedCallback will be the first callback to receive a callback if onBackPressed is called.

This method is notLifecycle aware - if you'd like to ensure that you only get callbacks when at least started, use addCallback. It is expected that you call remove to manually remove your callback.

open class MainActivity : AppCompatActivity(), HasAndroidInjector {
internal lateinit var binding: ActivityMainBinding

internal var onKeyboardChange: ((status: Int) -> Unit) = {}
...
...
...

private lateinit var keyboardObserver: KeyboardObserver
...
...
...

private lateinit var navController: NavController
private lateinit var appBarConfiguration: AppBarConfiguration
internal lateinit var firebaseAnalytics: FirebaseAnalytics
...
...
...

private val onBackPressedCallback: OnBackPressedCallback =
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (::navController.isInitialized) {
val navHostFragment = supportFragmentManager.fragments.first()

if (navHostFragment != null && navHostFragment.childFragmentManager.backStackEntryCount == 0) {
moveTaskToBack(true)
} else {
if (navHostFragment?.childFragmentManager?.backStackEntryCount == 1)
currentBottomNavItem = R.id.news_nav

navHostFragment?.childFragmentManager?.fragments?.let {
if (it.isNotEmpty()) {
when (it.first()) {
is CommentFragment, is PostFeedFragment -> {
onBackPressedDispatcher.onBackPressed()
}

else -> {
if (navController.popBackStack().not())
moveTaskToBack(true)
else
onBackPressedDispatcher.onBackPressed()
}
}
} else
showDefaultDialog("Something Wrong")
}
}
} else
handleOnBackPressed()
}
}

@Inject
internal lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>

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

...
...
...

FirebaseApp.initializeApp(this)
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
window.statusBarColor = ContextCompat.getColor(this, R.color.colorPrimaryDark)
if (localStorage.enabledDarkMode)
window.setSystemBarTextWhite()
else
window.setSystemBarTextDark()

...
...
...

// Obtain the FirebaseAnalytics instance.
firebaseAnalytics = Firebase.analytics
init(false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
}

...
...
...

}

You implemented this code to achieve onBackPressed in fragment. From now on all back press events will be captured by the handleOnBackPressed() and the fragment will have a chance to react to back press events. This code registers activity OnBackPressedDispatcher and gives you a callback in the current fragment.

abstract class BaseFragment<T : ViewBinding> : Fragment(), Injectable {
private var _binding: T? = null
abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> T

internal val binding
get() = _binding as T

internal var isLoading = false

internal lateinit var mainActivity: mainActivity

private lateinit var context: Context

...
...
...

private lateinit var onBackPressedCallback: OnBackPressedCallback

private var isFromBackStack = mutableMapOf<String, Boolean>()

protected open lateinit var navController: NavController

...
...
...

@Inject
internal lateinit var storage: Storage

@Inject
internal lateinit var putUserViewModelFactory: PutUserViewModel.AssistedVMFactory

@Inject
internal lateinit var postLogViewModelFactory: PostLogViewModel.AssistedVMFactory

//inner class Permission(val permissionName: String, val isGranted: Boolean)

open fun needTransparentToolbar() = false
open fun needSystemBarTextWhite() = false

companion object {
const val FRAGMENT_TAG = "fragment_tag"
...
...
...
}

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

...
...
...
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = _binding ?: bindingInflater.invoke(inflater, container, false)
mainActivity = (activity as MainActivity?)!!
(activity as MainActivity).supportActionBar?.hide()
navController =
(mainActivity.supportFragmentManager.fragments.first() as NavHostFragment).navController

return requireNotNull(_binding).root
}

override fun onResume() {
super.onResume()

mainActivity.addKeyboardListener()
setDarkMode(storage.enabledDarkMode)
}

override fun onPause() {
mainActivity.removeKeyboardListener()
hideKeyboard()

super.onPause()
}

override fun onDestroyView() {
super.onDestroyView()

_binding = null
}

override fun onDestroy() {
super.onDestroy()

_binding = null
}

override fun onAttach(context: Context) {
super.onAttach(context)

this.context = context
onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
handleBackPressed()
}
}

requireActivity().onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}

override fun onDetach() {
super.onDetach()

if (::onBackPressedCallback.isInitialized) {
onBackPressedCallback.isEnabled = false
onBackPressedCallback.remove()
}
}

override fun getContext() = context
...
...
...

protected open fun handleBackPressed() {}

}
class PostFeedFragment : BaseFragment<FragmentPostFeedBinding>() {
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentPostFeedBinding
get() = FragmentPostFeedBinding::inflate

private val args: PostFeedFragmentArgs by navArgs()

private val bodies = mutableListOf<ContentBody>()
private val contentBodies = mutableListOf<Any>()

private var shownKeyboard = false

...
...
...

@Inject
internal lateinit var postFeedViewModelFactory: PostFeedViewModel.AssistedVMFactory

companion object {
private const val POST_CONTENT_TYPE = "content"
}

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

shownKeyboard = args.shownKeyboard
storage.isFirstWrittenFeed = false
binding.etWithKeypadInputFeed.movementMethod = ScrollingMovementMethod()
setPostEnabled(false)
setOnListener()
if (shownKeyboard) {
openKeyboard("")
shownKeyboard = false
}
}

// Provide the custom back navigation handling behavior
override fun handleBackPressed() {
storage.feedEntryType = FROM_POST_FEED_JUST_BACK
setBackStackData("post_feed_state", data = false, doBack = true)
}

...
...
...
}

In MainActivity, you have overridden the method onBackPressed and in that you have checked the current active fragment and worked worked according to it. Now when I press the back button the onBackPressed event is triggered and you can check the currently active fragment and implement your custom code.

Now you can make this much easier by registering an OnBackPressedCallback instance to handle the ComponentActivity.onBackPressed() callback.

Thank you so much for reading and I hope this article was helpful. If you have something to say to me, please leave a comment, and if this article was helpful, please share and clicks 👏 👏 👏 👏 👏.

PS :

I’m scheduled to write a couple of tech article about Android Techs.

🔰 Next article coming soon! — ”How to make the item invisible on the list”

If you want to implement the effective ways to focus on your business logic & simplify modules & reduce the boiler-plate code & using the Module-Pattern(Called Module Pattern by Lukoh). I recommend reading my previous article: “How to focus effective business logic & implement more expandable & simplify modules & reduce the boiler-plate code using the Module-Pattern to make Android App.

I’m looking for a new job. Let me know or email me if you’re interested in my experience and techs. Please refer to my LinkedIn link below:

LinkedIn : https://www.linkedin.com/in/lukoh-nam-68207941/

Email : lukoh.nam@gmail.com

Android

Android App Development

--

--