Since the old Activity.onBackPressed() becomes deprecated starting Android 33, what is the better way to call it programmatically?
Example:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
// Handle default back arrow click
android.R.id.home -> {
onBackPressed()
}
...
We could create and add OnBackPressedCallback to the onBackPressedDispatcher like this.
onBackPressedDispatcher.addCallback(
this, // Lifecycle owner
backPressedCallback
)
private val backPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (viewPager.currentItem != 0)
viewPager.setCurrentItem(0, true)
else
finish()
}
}
Then replace the old onBackPressed with
// Handle default back arrow click
android.R.id.home -> {
backPressedCallback.handleOnBackPressed()
}
But I saw this public method in onBackPressedDispatcher and wondering if I could use it instead.
onBackPressedDispatcher.onBackPressed()
Does this method iterates on each OnBackPressedCallback that has been added in the onBackPressedDispatcher?
So basically onBackPressedDispatcher.onBackPressed() is the same as Activity.onBackPressed() and you can use it in the same manner if you don't care about precise navigation. How do I know that? - well, you can see the source code of the ComponentActivity(basically a parent of a regular Activity you are using), and its onBackPressed() looks like this:
#Override
#MainThread
public void onBackPressed() {
mOnBackPressedDispatcher.onBackPressed();
}
Regarding it calling over the callbacks queue - you are correct also, but it is not iterating over it - but just calling the most recent one - one at a time(per back press or per onBackPressed() call trigger), the documentation states:
public void onBackPressed()
Trigger a call to the currently added callbacks in reverse order in which they were added. Only if the most recently added callback is not enabled will any previously added callback be called.
If hasEnabledCallbacks is false when this method is called, the fallback Runnable set by the constructor will be triggered.
So your strategy here might be like this - if you need some specific stuff to be executed before the back navigation - you add it to the handleOnBackPressed of the callback. If no special behavior needed - you can just call mOnBackPressedDispatcher.onBackPressed() - it will still call the most recently added(if it is there of course) callback method, but if it is empty - the back will work just fine.
You need to keep in mind, though, that there are two overrides of addCallback methods:
addCallback(#NonNull OnBackPressedCallback onBackPressedCallback)
and
public void addCallback(
#NonNull LifecycleOwner owner,
#NonNull OnBackPressedCallback onBackPressedCallback
)
In the former - you have to handle the callback queue by yourself calling remove on callback when you need it not to be executed anymore. In the latter - LifecycleOwner state change has to handle all the needed stuff for you.
More info here and here.
Related
I have to use this peice of code twice in two different places in two different activites. No good programmer would willingly want to use same code in multiple places without reusing it.
//when back key is pressed
override fun onBackPressed() {
dialog.setContentView(twoBtnDialog.root)
twoBtnDialog.title.text = getString(R.string.warning)
twoBtnDialog.msgDialog.text = getString(R.string.backPressWarning)
twoBtnDialog.ok.text = getString(R.string.exit)
twoBtnDialog.cancel.text = getString(R.string.cancel)
twoBtnDialog.ok.setOnClickListener {
//do nav back
finish()
dialog.dismiss()
}
twoBtnDialog.cancel.setOnClickListener {
dialog.dismiss() //just do nothing
}
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialog.show()
}
I can move it to one place, but the problem is I have to pass in the finish() function from Activity Class to close the calling activity.
My simple question is how can I resue it ? Or How can I pass this function (finish()) to a different class (which is in some other file).
Take a function type parameter in your method.
fun doBackPress(finish: () -> Unit) {
// you need to invoke the finish method when necessary
finish.invoke()
}
Then you need to call the method and have to pass the finish() method from any other activity or fragment method like bellow.
override fun onBackPressed() {
doBackPress { finish() }
}
You could make an interface and extension function, which I think is less messy than trying to pass everything you need as parameters to a function, because it communicates intent better and makes it harder to do something wrong.
interface MyDialogOwner {
val dialog: Dialog
val twoBtnDialog: MyDialogBinding
fun Activity.handleBackPress() {
//the exact same content you have in your function now.
}
}
// In Activity:
override fun onBackPressed() = handleBackPress()
Your Activities should implement the interface, using your existing properties for dialog and twoBtnDialog (just add override in front of their declarations).
I'm assuming twoBtnDialog is a view binding.
I'm using single activity as a container for my fragments, my second fragment in its onCreate method runs a for loop in coroutine scope, but if the user presses the system's back button the app crashes with null pointer exception...How can I disable the back button functionality until my coroutine job is completed?
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setClickListeners()
//This creates buttons on the layout dynamically
GlobalScope.launch(Dispatchers.Main) {
delay(100)
for (i in 0 until runTillAndHowMany) {
createButton()
delay(25)
}
}
}
I know this is way too wrong to use GlobalScope like this in onCreate, but didn't find any alternative, I want that animation of custom buttons getting created one by one on screen.
I'm using NavigationComponent library, and Transition Animations
You could create a boolean variable to handle this. So in your Activity you can declare it like this:
var shouldGoBack: Boolean = false
And then you override your onBackPressed method to go as follows
override fun onBackPressed() {
if(shouldGoBack)
super.onBackPressed()
}
Finally you access the variable on your Fragment and set it to true once the coroutine is done like this:
(activity as YourActivity).shouldGoBack = true
Let me know if it works!
I recently updated my dependencies to include the OnBackPressedCallback change from an interface into an abstract class.
I have set things up according to the new documentation here but I feel like things are not working as they should.
My fragment's OnCreate looks a lot like the documentation:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requireActivity().onBackPressedDispatcher.addCallback(this) {
backPressed()
}
}
When I press the back button, the code in backPressed() is run, but nothing more happens.
I have tried calling handleBackPressed() and requireActivity().onBackPressedDispatcher.onBackPressed() and requireActivity().onBackPressed() from inside the callback, but those all cause a StackOverflowError because it seems to run that callback recursively.
There has got to be something really obvious I am missing...
There has got to be something really obvious I am missing...
You forget to disable your custom callback in you fragment before asking Activity to handle back pressed.
My solutiuon suitable for me:
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final OnBackPressedCallback callback = new OnBackPressedCallback(true) {
#Override
public void handleOnBackPressed() {
if (/*situation to handle back pressing*/){
//here handle your backPress in your fragment
} else {
setEnabled(false); //this is important line
requireActivity().onBackPressed();
}
}
};
requireActivity().getOnBackPressedDispatcher().addCallback(this, callback);
}
When you register an OnBackPressedCallback, you are taking on the responsibility for handling the back button. That means that no other on back pressed behavior is going to occur when you get a callback.
If you're using Navigation, you can use your NavController to pop the back stack:
requireActivity().onBackPressedDispatcher.addCallback(this) {
backPressed()
// Now actually go back
findNavController().popBackStack()
}
This works for me in androidx.appcompat:appcompat:1.1.0
requireActivity().onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
Log.d(TAG, "Fragment back pressed invoked")
// Do custom work here
// if you want onBackPressed() to be called as normal afterwards
if (isEnabled) {
isEnabled = false
requireActivity().onBackPressed()
}
}
}
)
You can also remove callback instead of setting enabled if it's no longer needed. I use it with nested graph like this because when you touch back in a nested nav graph with it's NavHostFragment, it removes it from main fragment back stack instead of opening last fragment in nested nav graph.
// Get NavHostFragment
val navHostFragment =
childFragmentManager.findFragmentById(R.id.nested_nav_host_fragment)
// ChildFragmentManager of the current NavHostFragment
val navHostChildFragmentManager = navHostFragment?.childFragmentManager
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val backStackEntryCount = navHostChildFragmentManager!!.backStackEntryCount
if (backStackEntryCount == 1) {
// We are at the root of nested navigation, remove this callback
remove()
requireActivity().onBackPressed()
} else {
navController?.navigateUp()
}
}
}
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
I have an activity with two fragments.
The second one is called when I click on something to the first.
What I want is this : if i click on "back" button, I want to go back to the first fragment (that is working), but I want to set the visibility to VISIBLE on an element (if the first fragment is called with back press only)
How do I do that ?
I tried something like this (in my main fragment), I've found the idea in another topic, but this is trigger always in my main activity :
override fun onResume() {
super.onResume()
view?.isFocusableInTouchMode = true
view?.requestFocus()
view?.setOnKeyListener { v, keyCode, event ->
if(event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK){
Log.i(TAG, "backpress pressed")
return#setOnKeyListener true
}
return#setOnKeyListener false
}
}
Temporary solution :
I've created a companion object with a value true or false and I change it everytime I need it, but it's temporary only.
Assuming your second Fragment replaces the first (i.e. using FragmentTransaction#replace), your first Fragment (we'll call them FragmentA and FragmentB) will be paused (i.e. onPause() will be called on FragmentA).
When you press the back button, the backstack will be popped, and FragmentA will be resumed (i.e. onResume() will be called).
What I would recommend, is to save a boolean flag in FragmentA, and set it to true when you show FragmentB. Then, in FragmentA#onResume, you can check if the flag is set to true, and set it back to false while handing the case that you wanted.
For example, something like:
private const val STATE_WAITING_FOR_FRAGMENT_B = "state_waiting_b"
class FragmentA : Fragment() {
private var isWaitingForFragmentB: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
isWaitingForFragmentB = savedInstanceState.getBoolean(STATE_WAITING_FOR_FRAGMENT_B)
}
}
override fun onResume() {
super.onResume()
if (isWaitingForFragmentB) {
isWaitingForFragmentB = false
// handle your view state here
}
}
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
savedInstanceState.putBoolean(
STATE_WAITING_FOR_FRAGMENT_B,
isWaitingForFragmentB
)
}
private fun showFragmentB() {
isWaitingForFragmentB = true
// do fragment transaction here
}
}
I'm not good at grammar.
First fragment do not call resume function when returning.
You must create callback with interface.
A good approach should be passing some flag, on the second fragment, by activity intent and to capture it on the first Fragment on onResume()
If you need extra info, just let me know
I am using the new android Navigation Framework in one of my Applications.
The purpose of Application is to behave as a launcher.
Sometimes when I try to change the fragment (navigate using the navcontroller) it doesn't change the fragment instead it logs
Ignoring navigate() call: FragmentManager has already saved its state
i know this question been asked before here Ignoring navigate() call: FragmentManager has already saved its state
but it doesn't have a Solution.
I am navigating using the following code:
Navigation.findNavController(view).navigate(R.id.action_next, bundle)
I had the same problem, in my case I was trying to use navigate() inside the Mopub ad callback onInterstitialDismissed, and was getting this info.
My solution for this case to use LiveData like this:
private var dismissState = MutableLiveData<Int>(0)
mMobupInterStitialAd?.interstitialAdListener = object : MoPubInterstitial.InterstitialAdListener {
override fun onInterstitialDismissed() {
dismissState.value=1
}
}
override fun onViewCreated() {
dismissState.observe(viewLifecycleOwner) {
if(it == 1) {
findNavController.navigate(R.id.fragmentAtoFragmentB)
}
}
}
This is how I solved the problem.