Proper usage of OnBackPressedDispatcher - android

Sometimes, I want to handle the back button being pressed by the user myself. My (example) code looks something like this:
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.test)
}
override fun onBackPressed() {
if (checkSomeCondition()) {
// do nothing
} else {
super.onBackPressed()
}
}
private fun checkSomeCondition() = false
}
I get notified when back is pressed, and then I decide if I want to handle it, or let the system handle it by calling super.onBackPressed().
Since onBackPressed() is now deprecated, I replace it by using the OnBackPressedDispatcher:
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.test)
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (checkSomeCondition()) {
// do nothing
} else {
onBackPressedDispatcher.onBackPressed()
}
}
})
}
private fun checkSomeCondition() = false
}
The problem with this code: by calling onBackPressedDispatcher.onBackPressed(), I call my own callback, creating an infinite loop instead of handing this over to the system like I would with super.onBackPressed().
This can be circumvented when temporarily disabling my callback like:
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (checkSomeCondition()) {
// do nothing
} else {
this.isEnabled = false
onBackPressedDispatcher.onBackPressed()
this.isEnabled = true
}
}
})
but that seems rather awkward and doesn't seem like it was intended that way.
What's the correct usage of the OnBackPressedDispatcher here that lets me either handle the back button press myself or hand it over to the system?
PS: I have seen this question about the deprecation of onBackPressed, but it doesn't answer my much more specific question.

As per the Basics of System Back video, the whole point of the Predictive Back gesture is to know ahead of time what is going to handle back.
That means that you should never have just-in-time logic such as checkSomeCondition as part of your call to handleOnBackPressed.
Instead, as explained in the Custom back navigation documentation, you should be updating the isEnabled state of your OnBackPressedCallback ahead of time whenever the conditions you used to check in checkSomeCondition changed. This ensures that your callback is only invoked when your condition is already true and is disabled when the condition is false, thus allowing the default behavior to occur.
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.test)
// Instead of always setting enabled to true at the beginning,
// you need to check the state ahead of time to know what the
// initial enabled state should be
val isConditionAlreadySet = checkSomeCondition()
val callback = object : OnBackPressedCallback(
isConditionAlreadySet
) {
override fun handleOnBackPressed() {
// Since you've handled isEnabled correctly, you know
// your condition is set correctly here, so you can
// unconditionally do what you need to do to handle back
}
}
// This is the key part - now you know ahead of time
// when the condition changes, which lets you control
// when you want to handle back or not
setOnSomeConditionChangedListener { isConditionMet ->
callback.isEnabled = isConditionMet
}
onBackPressedDispatcher.addCallback(this, callback)
}
private fun checkSomeCondition() = false
private fun setOnSomeConditionChangedListener(
(isConditionMet: Boolean) -> Unit
) {
// The implementation here will depend on what
// conditions checkSomeCondition() actually depends on
}
}

Related

Consume onBackPressed event using OnBackInvokedCallback in Activity?

According to the documentation from API 33 method onBackPressed() of Activity is deprecated. How can I use the suggested solution with OnBackInvokedCallback to prevent the parent method call?
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// how to consume action here to prevent super.onBackPressed() trigger?
}
})
}
This question concerns the case where there is a hierarchy of custom Activity classes, in one of which I want to prevent this method from being called in the parents in certain cases.
The desired behavior with deprecated API is as follows:
override fun onBackPressed() {
when {
needDoSomethingOnlyHere -> {
// do something
}
needDoSomethingElseOnlyHere -> {
// do something else
}
else -> {
super.onBackPressed()
}
}
}

onBackPressed() deprecated, What is the alternative?

I have upgraded targetSdkVersion and compileSdkVersion to 33.
Now getting warning onBackPressed is deprecated.
It is suggested to use use OnBackInvokedCallback or androidx.activity.OnBackPressedCallback to handle back navigation instead. Anyone can help me to use the updated method.
Example:
Use Case: I use if (isTaskRoot) {} inside onBackPressed(){} method to check activity is last on the activity-stack.
override fun onBackPressed() {
if (isTaskRoot) { // Check this activity is last on the activity-stack.(Check Whether This activity opened from Push-Notification)
startActivity(Intent(mContext, Dashboard::class.java))
finish()
} else {
finishWithResultOK()
}
}
According your API level register:
onBackInvokedDispatcher.registerOnBackInvokedCallback for API level 33+
onBackPressedDispatcher callback for backword compatibility "API level 13+"
This requires to at least use appcompat:1.6.0-alpha03; the current is 1.6.0-alpha04:
implementation 'androidx.appcompat:appcompat:1.6.0-alpha04'
// kotlin
import androidx.activity.addCallback
if (BuildCompat.isAtLeastT()) {
onBackInvokedDispatcher.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT
) {
// Back is pressed... Finishing the activity
finish()
}
} else {
onBackPressedDispatcher.addCallback(this /* lifecycle owner */, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Back is pressed... Finishing the activity
finish()
}
})
}
// ====================================================
/* Or for lambda simplicity: */
// ====================================================
if (BuildCompat.isAtLeastT()) {
onBackInvokedDispatcher.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT
) {
// Back is pressed... Finishing the activity
finish()
}
} else {
onBackPressedDispatcher.addCallback(this /* lifecycle owner */) {
// Back is pressed... Finishing the activity
finish()
}
}
UPDATE:
Thanks to #ianhanniballake comment; you can just use OnBackPressedDispatcher even in API level 33+
The OnBackPressedDispatcher is already going to be using the Android T specific API internally when using Activity 1.6+,
So, you can just do:
// kotlin
import androidx.activity.addCallback
onBackPressedDispatcher.addCallback(this /* lifecycle owner */, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Back is pressed... Finishing the activity
finish()
}
})
// ====================================================
/* Or for lambda simplicity: */
// ====================================================
onBackPressedDispatcher.addCallback(this /* lifecycle owner */) {
// Back is pressed... Finishing the activity
finish()
}
// java
import androidx.activity.OnBackPressedCallback;
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
#Override
public void handleOnBackPressed() {
// Back is pressed... Finishing the activity
finish();
}
});
Note that you shouldn't override the onBackPressed() as that will make the onBackPressedDispatcher callback not to fire; check this answer for clarifying that.
Replace onBackPressed() with below code.
onBackPressedDispatcher.onBackPressed()
With a combination of top answers. Here is a solution :
1. When you need to press the back button, copy this :
Note: it will automatically destroy your activity.
button.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
2. When you need to handle the back button pressed, copy this :
onBackPressedDispatcher.addCallback(this, object: OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Whatever you want
// when back pressed
println("Back button pressed")
finish()
}
})
Simply replace
override fun onBackPressed() {
super.onBackPressed() //Replace this is deprecated line
}
with
override fun onBackPressed() {
onBackPressedDispatcher.onBackPressed() //with this line
}
Update:
As we know override fun onBackPressed() is also deprecated so also replace this function with code below:
onBackPressedDispatcher.addCallback(this, object: OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
println("Back button pressed")
// Code that you need to execute on back press i.e. finish()
}
})
The above code we need to write inside some method like onCreate() function.
You could use the onBackPressedDispatcher
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
}
})
in here "this" means the lifeCycleOwner
You can use the OnBackInvokedCallback
as described in the documentation and follow this guide here to update your code
In Kotlin, this way is working
1- Remove onBackPressed()
2- below onCreate(savedInstanceState: Bundle?) add these lines:
if (Build.VERSION.SDK_INT >= 33) {
onBackInvokedDispatcher.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT
) {
exitOnBackPressed()
}
} else {
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
Log.i("TAG", "handleOnBackPressed: Exit")
exitOnBackPressed()
}
})
}
3- Define a new function for handling
fun exitOnBackPressed() {
}
Bonus: To close DrawerLayout when onBackPressed use like below (according to this I/O talk),
val callback = onBackPressedDispatcher.addCallback(this, false) {
binding.drawerLayout.closeDrawer(GravityCompat.START)
}
binding.drawerLayout.addDrawerListener(object : DrawerListener {
override fun onDrawerOpened(drawerView: View) {
callback.isEnabled = true
}
override fun onDrawerClosed(drawerView: View) {
callback.isEnabled = false
}
override fun onDrawerSlide(drawerView: View, slideOffset: Float) = Unit
override fun onDrawerStateChanged(newState: Int) = Unit
})
Here is the extension function to implement OnBackPressedCallback in activity.
fun AppCompatActivity.addOnBackPressedDispatcher(onBackPressed: () -> Unit = { finish() }) {
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
onBackPressed.invoke()
}
}
)
}
Usage:
addOnBackPressedDispatcher {
//doSomething()
}
use onBackPressedDispatcher.onBackPressed() instead of super.onBackPressed()
override fun onBackPressed() {
onBackPressedDispatcher.onBackPressed()
}
override the onDismiss() function for BottomSheets.
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
//TODO your logic.
}
This is the easy way to call the default Android BackButton since onBackPressed has been deprecated:
this#MyActivity.onBackPressedDispatcher.onBackPressed()
example:
myCloseButton.setOnClickListener { this#MyActivity.onBackPressedDispatcher.onBackPressed() }
Use like below,
override fun onClick(v: View?) {
when (v?.id) {
R.id.iv_back -> onBackPressedMethod()
}
}
and now create that method for handling back event
private fun onBackPressedMethod(){
if (Build.VERSION.SDK_INT >= 33) {
onBackInvokedDispatcher.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT) {
// back button pressed... finishing the activity
finish()
}
} else {
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// back button pressed... finishing the activity
finish()
}
})
}
}
That's it!

Check if first time using the app during launch

I'm trying to implement a first time using screen (like any other app when you have to fill some options before using the app for the first time).
I can't go to another Jetpack compose on an main activity on-create state because it check that every recomposition, and take me to the navigation path (I'd like to check the datastore entry once during launch), this what I already try, not seem to be working:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
val onBoardingStatus = dataStoreManager.onBoard.first()
setContent {
val navController = rememberNavController()
OnBoardingNavHost(navController)
navController.navigate(if (onBoardingStatus) "on_boarding" else "main") {
launchSingleTop = true
popUpTo(0)
}
}
}
}
it is possible to check that only once (in application class for example and not in oncreate?)
please advice,
thanks in advance
You have to use LaunchedEffect for this, you can do something like this
enum class OnboardState {
Loading,
NoOnboarded,
Onboarded,
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var onboardingState by remember {
mutableStateOf(OnboardState.Loading)
}
LaunchedEffect(Unit) {
onboardingState = getOnboardingState()
}
when (onboardingState) {
OnboardState.Loading -> showSpinner()
OnboardState.NoOnboarded -> LaunchedEffect(onboardingState) {
navigateToOnboarding()
}
OnboardState.Onboarded -> showContent()
}
}
}

Fragment loses listener at orientation change

I have an activity using fragments. To communicate from the fragment to the activity, I use interfaces. Here is the simplified code:
Activity:
class HomeActivity : AppCompatActivity(), DiaryFragment.IAddEntryClickedListener, DiaryFragment.IDeleteClickedListener {
override fun onAddEntryClicked() {
//DO something
}
override fun onEntryDeleteClicked(isDeleteSet: Boolean) {
//Do something
}
private val diaryFragment: DiaryFragment = DiaryFragment()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
diaryFragment.setOnEntryClickedListener(this)
diaryFragment.setOnDeleteClickedListener(this)
supportFragmentManager.beginTransaction().replace(R.id.content_frame, diaryFragment)
}
}
The fragment:
class DiaryFragment: Fragment() {
private var onEntryClickedListener: IAddEntryClickedListener? = null
private var onDeleteClickedListener: IDeleteClickedListener? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view: View = inflater.inflate(R.layout.fragment_diary, container, false)
//Some user interaction
onDeleteClickedListener!!.onEntryDeleteClicked()
onDeleteClickedListener!!.onEntryDeleteClicked()
return view
}
interface IAddEntryClickedListener {
fun onAddEntryClicked()
}
interface IDeleteClickedListener {
fun onEntryDeleteClicked()
}
fun setOnEntryClickedListener(listener: IAddEntryClickedListener) {
onEntryClickedListener = listener
}
fun setOnDeleteClickedListener(listener: IDeleteClickedListener) {
onDeleteClickedListener = listener
}
}
This works, but when the fragment is active and the orientation changes from portrait to landscape or otherwise, the listeners are null. I can't put them to the savedInstanceState, or can I somehow? Or is there another way to solve that problem?
Your Problem:
When you switch orientation, the system saves and restores the state of fragments for you. However, you are not accounting for this in your code and you are actually ending up with two (!!) instances of the fragment - one that the system restores (WITHOUT the listeners) and the one you create yourself. When you observe that the fragment's listeners are null, it's because the instance that has been restored for you has not has its listeners reset.
The Solution
First, read the docs on how you should structure your code.
Then update your code to something like this:
class HomeActivity : AppCompatActivity(), DiaryFragment.IAddEntryClickedListener, DiaryFragment.IDeleteClickedListener {
override fun onAddEntryClicked() {
//DO something
}
override fun onEntryDeleteClicked(isDeleteSet: Boolean) {
//Do something
}
// DO NOT create new instance - only if starting from scratch
private lateinit val diaryFragment: DiaryFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
// Null state bundle means fresh activity - create the fragment
if (savedInstanceState == null) {
diaryFragment = DiaryFragment()
supportFragmentManager.beginTransaction().replace(R.id.content_frame, diaryFragment)
}
else { // We are being restarted from state - the system will have
// restored the fragment for us, just find the reference
diaryFragment = supportFragmentManager().findFragment(R.id.content_frame)
}
// Now you can access the ONE fragment and set the listener on it
diaryFragment.setOnEntryClickedListener(this)
diaryFragment.setOnDeleteClickedListener(this)
}
}
Hope that helps!
the short answer without you rewriting your code is you have to restore listeners on activiy resume, and you "should" remove them when you detect activity losing focus. The activity view is completely destroyed and redrawn on rotate so naturally there will be no events on brand new objects.
When you rotate, "onDestroy" is called before anything else happens. When it's being rebuilt, "onCreate" is called. (see https://developer.android.com/guide/topics/resources/runtime-changes)
One of the reasons it's done this way is there is nothing forcing you to even use the same layout after rotating. There could be different controls.
All you really need to do is make sure that your event hooks are assigned in OnCreate.
See this question's answers for an example of event assigning in oncreate.
onSaveInstanceState not working

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?
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 in your Fragment:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
// Show your dialog and handle navigation
}
// you can enable/disable the callback here by setting
// callback.isEnabled = true/false. Or just enable it in the lambda.
}
That addCallback method takes in a LifecycleOwner, and it will make sure that the callback is added when LifecycleOwner reaches the STARTED stage. Not only that, but this also makes it so that the callback is removed when its associated LifecycleOwner is destroyed.
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.
With the navigation architecture components, you can do something like this:
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)
}
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()
}
}
}
for up navigation simply override onOptionsItemSelected()
override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) {
android.R.id.home -> {
showDialog() // show your dialog here
true
}
else -> super.onOptionsItemSelected(item)
}
You can used following function in onAttach in your fragment to override the onBackPressed() with help of the navigation components.
requireActivity().onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (YOUR_CONDITION) {
// Do something here
} else {
if (!findNavController().navigateUp()) {
if (isEnabled) {
isEnabled = false
requireActivity().onBackPressedDispatcher.onBackPressed()
}
}
}
}
}
)
If you're using it with AppBarConfiguration, with the latest release there is now an AppBarConfiguration.OnNavigateUpListener. Refer to the below link for more information
https://developer.android.com/reference/androidx/navigation/ui/AppBarConfiguration.OnNavigateUpListener
if u override onBackPressed() in your activity must ensure that it should call super.onBackOnBackPressed() otherwise these dispatcher wont trigger

Categories

Resources