I have an Activity B that extends AppCompatActivity
I add (not replace) two fragments and also add them to the back stack
Activity B
Fragment 1
Fragment 2
supportFragmentManager
.beginTransaction()
.addToBackStack(null)
.add(containerId, fragment, fragment::class.java.simpleName)
.commit()
My Activity's onBackPress():
override fun onBackPressed() {
if (fragmentBackStackCount == 1) {
super.onBackPressed()
} else {
supportFragmentManager.popBackStack()
}
}
I want to go back to Activity A but I face a blank white screen (Activity B)
How can I handle fragments back stack correctly?
Try this code
override fun onBackPressed() {
if (fragmentBackStackCount <= 1) {
finish()
}
else {
super.onBackPressed()
}
}
Open Fragment with tag
supportFragmentManager.beginTransaction().addToBackStack(tag)
.add(R.id.flContainer, fragment, tag)
.commitAllowingStateLoss()
and Activity onBackPressed remove fragment
override fun onBackPressed() {
val secondFragment =
supportFragmentManager.findFragmentByTag("YoursecondFragmentTagName")
if (secondFragment != null){
// Here remove all fragment
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStack(null,
FragmentManager.POP_BACK_STACK_INCLUSIVE)
}
}else
super.onBackPressed()
}
Hope this help you...
Related
My task is to dynamically go on back pressed when i want to go back from fragment.
I have bottom navigation which is in activity.
My Code:
MainActivity:
setCurrentFragment(topArticlesFragment)
binding.bottomNavigationView.setOnItemSelectedListener {
when (it.itemId) {
R.id.topArticles -> setCurrentFragment(topArticlesFragment)
R.id.allArticles -> setCurrentFragment(everythingArticlesFragment)
}
return#setOnItemSelectedListener true
}
private fun setCurrentFragment(fragment: Fragment) {
supportFragmentManager
.beginTransaction().apply {
replace(R.id.frame_layout, fragment)
this.addToBackStack(null)
commit()
}
}
TopArticlesFragment:
binding.recyclerViewTop.visibility = GONE
binding.searchView.visibility = GONE
parentFragmentManager
.beginTransaction()
.replace(R.id.topArticlesFragment, fragment)
.addToBackStack(null)
.commit()
}
DetailsFragment:
binding.toolbar.setNavigationOnClickListener {
activity?.onBackPressed()
}
Pictures of what is happening
I also tried with nav_graph but i put bottom navigation in MainFragment and when i choose another fragment bottom navigation disappeared.
Edit:
EDIT:
I also have onResume and onStop in TopArticleFragment and Everything article fragment to show and hide topMenu and bottom navigation. But when used binding.recyclerView.isVisible = true nothing happens.
override fun onResume() {
super.onResume()
(activity as AppCompatActivity?)!!.supportActionBar!!.show()
(activity as AppCompatActivity?)!!.findViewById<BottomNavigationView>(R.id.bottomNavigationView).isVisible =
true
}
override fun onStop() {
super.onStop()
(activity as AppCompatActivity?)!!.supportActionBar!!.hide()
}
i have 1 Activity with 3 Fragments. (A, B and C). So,
Activity -> FragmentContainerView with fragment A
<androidx.fragment.app.FragmentContainerView
android:id="#+id/host_fragment"
android:name="cl.gersard.shoppingtracking.ui.product.list.ListProductsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="ListProductsFragment" />
Fragment A has a button to go to Fragment B
Fragment A -> Fragment B (with addToBackStack)
Then, i go to from Fragment B to Fragment C
Fragment B -> Fragment C (without addToBackStack)
i need when i save a item in Fragment C, come back to Fragment A, so i dont use addToBackStack.
The problem is when in Fragment C i use
requireActivity().supportFragmentManager.popBackStack()
or
requireActivity().onBackPressed()
the Fragment A appears but the method OnViewCreated in Fragment C is called so execute a validations that i have in that Fragment C.
I need from Fragment C come back to Fragment A without calling OnViewCreated of Fragment C
Code of interest
MainActivity
fun changeFragment(fragment: Fragment, addToBackStack: Boolean) {
val transaction = supportFragmentManager.beginTransaction()
.replace(R.id.host_fragment, fragment,fragment::class.java.simpleName)
if (addToBackStack) transaction.addToBackStack(null)
transaction.commit()
}
Fragment A (ListProductsFragment)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
observeLoading()
observeProducts()
viewModel.fetchProducts()
viewBinding.btnEmptyProducts.setOnClickListener { viewModel.fetchProducts() }
viewBinding.fabAddPurchase.setOnClickListener { addPurchase() }
}
private fun addPurchase() {
(requireActivity() as MainActivity).changeFragment(ScanFragment.newInstance(),true)
}
Fragment B (ScanFragment)
override fun barcodeDetected(barcode: String) {
if (processingBarcode.compareAndSet(false, true)) {
(requireActivity() as MainActivity).changeFragment(PurchaseFragment.newInstance(barcode), false)
}
}
Fragment C (PurchaseFragment)
private fun observePurchaseState() {
viewModel.purchasesSaveState.observe(viewLifecycleOwner, { purchaseState ->
when (purchaseState) {
is PurchaseSaveState.Error -> TODO()
is PurchaseSaveState.Loading -> manageProgress(purchaseState.isLoading)
PurchaseSaveState.Success -> {
Toast.makeText(requireActivity(), getString(R.string.purchase_saved_successfully), Toast.LENGTH_SHORT).show()
requireActivity().supportFragmentManager.popBackStack()
}
}
})
}
The full code is here https://github.com/gersard/PurchaseTracking
OK, I think I see your issue. You are conditionally adding things to the backstack which put the fragment manager in a weird state.
Issue
You start on Main, add the Scanner to the back stack, but not the Product. So when you press back, you're popping the Scanner off the stack but the Product stays around in the FragmentManager. This is why get a new instance each and every time you scan and go back. Why this is happening is not clear to me - seems like maybe an Android bug? You are replacing fragments so it's odd that extra instances are building up.
One Solution
Change your changeFragment implementation to conditionally pop the stack instead of conditionally adding things to it.
fun changeFragment(fragment: Fragment, popStack: Boolean) {
if (keepStack) supportFragmentManager.popBackStack()
val transaction = supportFragmentManager.beginTransaction()
.replace(R.id.host_fragment, fragment,fragment::class.java.simpleName)
transaction.addToBackStack(null) // Always add the new fragment so "back" works
transaction.commit()
}
Then invert your current logic that calls changeFragment:
private fun addPurchase() {
// Pass false to not pop the main activity
(requireActivity() as MainActivity)
.changeFragment(ScanFragment.newInstance(), false)
}
And ...
override fun barcodeDetected(barcode: String) {
if (processingBarcode.compareAndSet(false, true)) {
// Pass true to pop the barcode that's currently there
(requireActivity() as MainActivity)
.changeFragment(PurchaseFragment.newInstance(barcode), true)
}
}
I have this scenario on my MainActivity:
// onCreate
firebaseAuth.addAuthStateListener { firebaseAuth ->
when (firebaseAuth.currentUser) {
null -> {
hideAppBars()
clearBackStack(supportFragmentManager)
showFragment(fragment = LoginOrRegisterFragment())
}
else -> {
showAppBars()
clearBackStack(supportFragmentManager)
showFragment(fragment = HomeFragment())
}
}
}
The clearBastack is just a method that is popping the full backstack of the Fragments:
private fun clearBackStack(fragmentManager: FragmentManager) {
with(fragmentManager) {
if (backStackEntryCount > 0)
popBackStack()
}
}
And showFragment method:
fun showFragment(fragment: Fragment, addToBackStack: Boolean = false) {
supportFragmentManager.beginTransaction().apply {
replace(R.id.fragmentContainer, fragment)
if (addToBackStack) addToBackStack(null)
}.commit()
}
In a usual flow, everything goes OK. Hit Login: BackStack clears and from LoginFragment I get to HomeFragment. However, if I press back when I'm in the LoginFragment and resume , I get IllegalStateException: FragmentManager has been destroyed
What seems to fix the issue
Explicitly checking if(!supportFragmentManager.isDestroyed):
fun showFragment(fragment: Fragment, addToBackStack: Boolean = false) {
if (!supportFragmentManager.isDestroyed) {
supportFragmentManager.beginTransaction().apply {
replace(R.id.fragmentContainer, fragment)
if (addToBackStack) addToBackStack(null)
}.commit()
}
}
UPDATE: Full stacktrace:
java.lang.IllegalStateException: FragmentManager has been destroyed
at androidx.fragment.app.FragmentManager.enqueueAction(FragmentManager.java:1725)
at androidx.fragment.app.BackStackRecord.commitInternal(BackStackRecord.java:321)
at androidx.fragment.app.BackStackRecord.commit(BackStackRecord.java:286)
at com.coroutinedispatcher.datacrypt.MainActivity.showFragment(MainActivity.kt:57)
at com.coroutinedispatcher.datacrypt.MainActivity.showFragment$default(MainActivity.kt:52)
at com.coroutinedispatcher.datacrypt.MainActivity$onCreate$1.onAuthStateChanged(MainActivity.kt:36)
at com.google.firebase.auth.zzj.run(com.google.firebase:firebase-auth##19.4.0:3)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at com.google.android.gms.internal.firebase_auth.zzj.dispatchMessage(com.google.firebase:firebase-auth##19.4.0:6)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
Line that throws is supportFragmentManager.apply{bla()}.commit().
Question is, why, what is actually happening in the background?
You should remove the AuthStateListnener in onDestroy of Activity.
// onCreate
private val authStateListener = AuthStateListener { firebaseAuth ->
when (firebaseAuth.currentUser) {
null -> {
hideAppBars()
clearBackStack(supportFragmentManager)
showFragment(fragment = LoginOrRegisterFragment())
}
else -> {
showAppBars()
clearBackStack(supportFragmentManager)
showFragment(fragment = HomeFragment())
}
}
}
override fun onCreate(...) {
super.onCreate(...)
firebaseAuth.addAuthStateListener(authStateListener)
}
override fun onDestroy() {
firebaseAuth.removeAuthStateListener(authStateListener)
super.onDestroy()
}
Although technically you should also consider that this could still trigger fragment transactions after onStop, which would cause a this action cannot be performed after onSaveInstanceState error, so you should actually only handle the navigation action if the Activity is at least started.
You could use https://github.com/Zhuinden/live-event for example for that.
I have an activity with two fragments added to it using :
fun addFragment(fragment: Fragment){
private val fragmentManager = supportFragmentManager
val fragmentTag = fragment::class.simpleName.toString()
val ft = fragmentManager.beginTransaction()
ft.add(R.id.fragments_container, fragment,fragmentTag)
ft.addToBackStack(fragmentTag)
ft.commit()
}
I add two fragments A and B to activity with this order:
A ---> B
when i press back button on phone it return from B to A as expected
but the problem is that when i call activity's onBackPressed method when clicking on a view for example:
imgBack.setOnClickListener {
onBackPressed()
}
it does not work like when i press back button on the phone
it returns to fragment A but not showing fragment A views as expected.
onBackPressed:
override fun onBackPressed() {
if (fragmentManager.backStackEntryCount > 1) {
fragmentManager.popBackStack()
} else {
finish()
}
}
if you imgBack are in fragmentB call
imgBack.setOnClickListener {
activity?.onBackPressed()
}
Here's my attempt:
private inline fun FragmentManager.inTransaction(func: FragmentTransaction.() -> FragmentTransaction) {
beginTransaction().func().addToBackStack(null).commit()
}
private fun AppCompatActivity.addFragment(fragment: Fragment, frameId: Int){
supportFragmentManager.inTransaction { add(frameId, fragment) }
}
private fun AppCompatActivity.showFragment(fragment: Fragment) {
supportFragmentManager.inTransaction{show(fragment)}
}
private fun showFragmentView(fragment: Fragment){
// Hide the current Fragment
if (supportFragmentManager.fragments.isNotEmpty()) {
val currentFragment = supportFragmentManager.fragments.last()
if (currentFragment != null) {
supportFragmentManager
.beginTransaction()
.hide(currentFragment)
.commit()
}
}
// Add or Show
if (!fragment.isAdded) {
addFragment(fragment, sendFragFrame.id)
} else {
showFragment(fragment)
}
}
It properly adds the fragment to the frame, but when I attempt to hide it nothing happens, it's stays visible and the second fragment cannot be seen. Can someone explain why this is happening?
It is not good practice (and most likely won't work) to access fragments in this way.
if (supportFragmentManager.fragments.isNotEmpty()) {
val currentFragment = supportFragmentManager.fragments.last()
if (currentFragment != null) {
supportFragmentManager
.beginTransaction()
.hide(currentFragment)
.commit()
You should add fragments with a tag and get the fragment by tag when you want to remove it and then do your transaction.
See the comments on this for more:
How do I get the currently displayed fragment?
Well embarrassingly my problem was that I had inadvertently swap my layout in Fragment2 with the layout for Fragment1...so it WAS working, but because they shared a layout you couldn't see it visually. I'd delete this post if I could, but I'll leave it here as tribute to my shame as a developer.