Android navigation component display multiple DialogFragments in a row - android

Hi folks I need to throw up a series of DialogFragments one after another using the navigation component. I have encountered some pretty unusual system behavior which looks like a race condition. I show the dialogs with a global action after an item is clicked, and use the fragment result api to determine if another one should be shown.
I am using a custom layout so there's no positive / negative listeners etc., and my own continue / cancel buttons send a result back to the host fragment.
ItemsFragment.kt:
navController.navigate(RegisterItemsDirections.openModsDialog(item.id, 0))
fragment.setFragmentResultListener(ItemsFragment.MODIFIERS_REQUEST) { key, bundle ->
//kill the current dialog
navController.navigateUp()
//some logic to determine if we need another dialog...
if(needAnotherDialog){
//navigate to the next one
navController.navigate(RegisterItemsDirections.openModsDialog(item.id, lastModGroupSelectionIndex + 1))
}
}
ModsDialogFragment.kt, when the user clicks "continue"
setFragmentResult(MODIFIERS_REQUEST, bundleOf(MODIFIERS_RESULT to selectedMods))
So the issue only appears on 3rd or more dialogs on my devices, I can see that the 1st and 2nd dialogs are completely destroyed and detached. When it displays the 3rd one, the first one attaches itself again, and appears beneath the 3rd one which I can't explain.
I've tried:
Popping the back stack in the global action's nav xml
Navigating up or dismissing the dialog fragment in the dialog itself (before calling setFragmentResult), which is the most logical place to put it
Popping the backstack instead of navigating up, basically the same thing in this case
When I don't dismiss / nav up any of the dialogs and allow them all to just stack, there's no issue. When I delay the navigation by 500ms there is no issue. So navving up and then immediately navigating to another instance of the dialog are fighting with eachother producing very strange results. What could be the solution here that doesn't involve a random delay?
Navigation version is 2.3.3 and I'm having a lot of trouble trying to update it due to AGP upgrades being incompatible with a jar I need so I'm not sure if this has been fixed.

I figured out the issue, it's down to dumb copy pasting.
I took the donut tracker sample code and made it stack dialogs in the same way and there was no issue.
The difference between the two was I am subclassing DialogFragment and for some unknown reason doing this:
override fun show(manager: FragmentManager, tag: String?) {
val transaction = manager.beginTransaction()
val prevFragment = manager.findFragmentByTag(tag)
if (prevFragment != null) {
transaction.remove(prevFragment)
}
transaction.addToBackStack(null)
show(transaction, tag)
}
This code predates the Nav component library I believe, and it completely f***s with the fragment manager used by the navigation component. So the moral of the story is to not do bizarre things in super classes, or better yet not super class at all.

Related

Navigation Component how to make each fragment only has one instance all the time

My project
Single activity pattern with fragments in kotlin.
Navigation component + bottom navigation view together.
There are four tabs(fragments) in bottom navigation view.
My issue is changing each tab in bottom navigation, then each fragment is re-created which due to the app is laggy.
So my target is making only one instance of each fragment there.
What I tried is:
adding app:launchSingleTop="true" for the tab fragment in grap.xml. DOESN'T WORK.
This idea is if the tab fragment can be pop backed then use it directly or create new. But this only works sometimes. Some times the tab fragment does not re-created but some times are!
I think the reason is pop back stack clear it for some time? Not sure.
binding.bottomNavigationView.setOnItemSelectedListener { item: MenuItem ->
if (!navController.popBackStack(item.itemId, false)) {
NavigationUI.onNavDestinationSelected(item , navController)
}
true
}
I used navController.navigate(item.itemId, null, NavOptions.Builder().setPopUpTo(item.itemId, false).build()) to replace NavigationUI.onNavDestinationSelected(item , navController), still doesn't work.
Any idea? thanks!
just add this piece of code to avoid recreation
binding.bottomNavigationView.setOnItemReselectedListener { }

Custom DialogFragment - Screen Rotation Crash - Fragment XXX has not been attached yet

I am stuck with this problem from few hours now.
I have a Android App, There is Activity A. This activity has bottom navigation drawer, that uses view pager, I have 3 views, account, contact and notes.
On each view you can see list of items, as in contacts you can see list of contacts added, on notes you can see list of notes added and so on.
There is fab icon on every fragment that is basically add/edit button, when clicked it opens a dialogfragment which is used for editing or adding contacts or notes.
My problem is that I can only open my Edit dialog for Contact and notes one time. Once you rotate the screen 2-3 times, I get the error .. Fragment ContactDialogXXXX has not been attached yet.
fun editContactDialog(contact: Contact?) {
val transaction = childFragmentManager.beginTransaction()
val previous = childFragmentManager.findFragmentByTag(ContactEditDialog.name)
if (previous != null) {
transaction.remove(previous)
}
val dialog = ContactEditDialog.getInstance(contact, accountId)
dialog.show(transaction, ContactEditDialog.name)
}
I am using this above method to open dialog everytime. and only gives me error that Fragment XX has not been attached yet. after rotation. I have even done below thing from given links with no luck.
set retainInstance to true in onCreate
also added this snippet
override fun onDetach() {
super.onDetach()
callbacks = null
}
I did add
if (!isAdded)
return
but adding above line in that method only stops the crash but my dialog does not open then.
I have searched everywhere online, looked into all possible combinations on stackoverflow but my problem persists.
tried solutions from below places:
Passing an Object to Fragment or DialogFragment upon Instantiation
https://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html
https://guides.codepath.com/android/using-dialogfragment#styling-dialogs
What am I missing? Is there another way of doing it? A better way of creating error prone Custom Dialog Fragments?

FragmentScenario and nested NavHostFragments don't perform navigations as expected in Instrumentation tests

I am writing a single Activity app that uses Android's Navigation Components to help with navigation and Fragment Scenario for instrumentation testing. I have run into a performance discrepancy when using the back button between the actual app navigation behavior and the behavior of a Fragment being tested in isolation during an Instrumentation tests when using fragment scenario.
In my MainActivity I have a main NavHostFragment that takes up the entire screen. I use that nav host fragment to show several screens including some master detail fragments. Each master detail fragment has another NavHostFragment in it to show the different detail fragments for that feature. This setup works great and provides the behavior I desire.
To accomplish the master detail screen I use a ParentFragment that has two FrameLayouts to create the split screen for tablet and for handset I programatically hide one of the FrameLayouts. When the ParentFragment is created, it detects if it is being run on a tablet or handset and then programatically adds a NavHostFragment to the right frame layout on tablet, and on handset hides the right pane adds a NavHostFragment to the left pane. The NavHostFragments also have a different navigation graph set on them depending on if they are being run on tablet or handset (on handset we show fragments as dialogs, on tablet we show them as regular fragments).
private fun setupTabletView() {
viewDataBinding.framelayoutLeftPane.visibility = View.VISIBLE
if (navHostFragment == null) {
navHostFragment = NavHostFragment.create(R.navigation.transport_destinations_tablet)
navHostFragment?.let {
childFragmentManager.beginTransaction()
.add(R.id.framelayout_left_pane, it, TRANSPORT_NAV_HOST_TAG)
.setPrimaryNavigationFragment(it)
.commit()
}
}
if (childFragmentManager.findFragmentByTag(SummaryFragment.TAG) == null) {
childFragmentManager.beginTransaction()
.add(R.id.framelayout_right_pane, fragFactory.instantiate(ClassLoader.getSystemClassLoader(), SummaryFragment::class.java.canonicalName!!), SummaryFragment.TAG)
.commit()
}
}
private fun setupPhoneView() {
viewDataBinding.framelayoutLeftPane.visibility = View.GONE
if (navHostFragment == null) {
navHostFragment = NavHostFragment.create(R.navigation.transport_destinations_phone)
navHostFragment?.let {
childFragmentManager.beginTransaction()
.replace(R.id.framelayout_left_pane, it, TRANSPORT_NAV_HOST_TAG)
.setPrimaryNavigationFragment(it)
.commit()
}
}
}
When running the devDebug version of the app, everything works as expected. I am able to navigate using the main NavHostFragment to different master-detail screens. After I navigate to the master-detail screen, the nested NavHostFragment takes over and I can navigate screens in and out of the master detail fragment using the nested NavHostFragment.
When the user attempts to click the back button, which would cause the to leave the master detail screen and navigate to the previous screen, we pop up a dialog to the user asking if they really want to leave the screen (it's a screen where they enter a lot of data). To accomplish this we register an onBackPressDispatcher callback so we know when the back button was pressed and navigate to the dialog when the callback is invoked. In the devDebug version, the user begins by being at location A on the nav graph. If, when they are at location A, they click the back button, then we show a dialog fragment asking if the user really intends to leave the screen. If, instead, the user navigates from location A to location B and clicks back they are first navigated back to location A. If they click the back button again, the back press dispatcher callback is invoked and they are then shown the dialog fragment asking if they really intent to leave location A. So it seems that that the back button affects the back stack of the nested NavHostFragment until the nested NavHostFragment only has one fragment left. When only one fragment is left and the back button is clicked, the onBackPressDisapatcher callback is invoked. This is exactly the desired behavior. However, when I write an Instrumentation test with Fragment Scenario where I attempt to test the ParentFragment I have found that the back press behavior is different. In the test I use Fragment Scenario to launch ParentFragment, I then run a test where I do a navigation in the nested NavHostFragment. When I click the back button I expect that the nested nav host fragment will pop its stack. However, the onBackPressDispatcher callback is invoked immediately instead of after the nested nav host fragment has one fragment left on its stack.
I set some breakpoints in the NavHostFragment and it seems that when the tests are run, the NavHostFragment is not setup to intercept back clicks. Its enableOnBackPressed() method is always called with a flag set to false.
I don't understand what about the test setup is causing this behavior. I would think that the nav host fragment would intercept the back clicks itself until it only had one fragment left on its backstack and only then would the onBackPressDispatcher callback be invoked.
Am I misunderstanding how I should be testing this? Why does the onBackPressDispatcher's callback get called when the back button is pressed.
As seen in the FragmentScenario source code, it does not currently (as of Fragment 1.2.1) use setPrimaryNavigationFragment(). This means that the Fragment being tested does not intercept the back button and hence, its child fragments (such as your NavHostFragment) do not intercept the back button.
You can set this flag yourself in your test:
#Test
fun testParentFragment() {
// Use the reified Kotlin extension to launchFragmentInContainer
with(launchFragmentInContainer<ParentFragment>()) {
onFragment { fragment ->
// Use the fragment-ktx commitNow Kotlin extension
fragment.parentFragmentManager.commitNow {
setPrimaryNavigationFragment(fragment)
}
}
// Now you can proceed with your test
}

Initial Fragment not rendered when popping entire FragmentManager BackStack

Actual problem / TL;DR: Except when using one way I consider a hack, I can't pop the activity's entire backstack to return to my initial state with a single fragment added during onCreate(). The Fragment is present in the Activity State Manager dump, but is not visible, leaving me with an empty screen.
I have encountered this issue today and am mostly trying to understand whether this is caused by a bug or by my misunderstanding of the FragmentManager's BackStack. I want to make sure I'm not misusing Fragments or building on shaky API foundations.
I have an activity which essentially offers a descending "tree" navigation as a grid of buttons where each button opens either a sub-grid (e.g. sub-category with more buttons) or some custom form. The content of the forms is irrelevant, but users are expected to repetitively fill them in various orders.
I use Fragments from the support library (support-v4:25.1.1) and whenever a new "screen" is required (either for a form or a sub-grid) it will be added using a transaction on the activity's FragmentManager similar to:
/* adding a new screen, going further down our nav tree */
fragmentManager
.beginTransaction()
.addToBackStack("...")
.replace(R.id.container, newFragment)
.commit();
Every transaction looks like that except the setup of the initial state of my activity, which adds the initial "root grid" fragment without adding the transaction to the backstate.
/* adding the initial fragment during the first execution of activity's onCreate(). */
fragmentManager
.beginTransaction()
.replace(R.id.container, rootFragment)
.commit();
The reason I'm posting here is that I encountered a very strange behaviour when attempting to pop the entire BackStack, which is supposed to bring the user back to the initial state of the activity which has the root grid (the one added in the code above). Almost every technique I tried effectively clears the backstack, but leaves an empty screen instead of my initial main-menu grid. After looking around on multiple questions here and the docs, I tried multiple solutions and only this hack-looking one has worked:
int remainingEntries = fragmentManager.getBackStackEntryCount();
while (remainingEntries-- > 0) {
fragmentManager.popBackStackImmediate();
}
I say this seems like a hack because it requires me to immediately (synchronously) pop an arbitrary number of backstack entries to reach the root, whereas from my understanding it should be possible to do that with a single asynchronous method call:
fragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
or as I've also seen posted around here:
int id = fragmentManager.getBackStackEntryAt(0).getId();
fragmentManager.popBackStack(id, FragmentManager.POP_BACK_STACK_INCLUSIVE);
This is only an issue with the root fragment, probably because the transaction which adds it to the view is not added to the backstack, since that would cause an extra empty screen to be shown when the user presses the back button to close the activity.
More details:
According to the Activity State Manager dump my fragment still has its view, but has both mFragmentId and mContainerId set to #0.
I do add another fragment I haven't discussed here which retains its instance, but it is only used to hold some data and has no view whatsoever.
There is a PopBackStackImmediate bug in support library version 25.1.0 and above.
Since this issue has been reported either you have to wait for solution, Or you can downgrade the support library version to 25.0.1 and get the expected behavior.
UPDATE
Looks like this bug has been resolved in support library version 25.3.1. Update your library to version 25.3.1.

How to avoid multiple instances of fragments in Activity after app is killed and resumed?

I have an app with a Home screen that has 2 fragments (for now) and a navigation drawer. Currently I load the fragment A (Explore) on startup and load fragment B when clicked. From then on, I show and hide fragments. It's faster than recreating fragments on every click and my fragment A takes some time to load.
I've noticed that when I go to fragment B and go to another activity (let's call it activity 2) from there and leave the app and wait for it to be killed (or do something crazy like change the device language), and then come back to the same activity, it's still there. When I press back to go back to fragment B, sometimes (50% of times) the fragment B is drawn over fragment A. On clicking fragment A in the drawer, fragment A appears fine, but on clicking fragment B, there's another instance of fragment A and on top of that fragment B.
I've spent more than 2 days on this problem and got nowhere.
Here's my code for selecting the fragment:
private void selectItem(int position, boolean addExploreFragment) {
Log.d(tag, "selectItem: " + position);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
//add explore fragment - this is called on app startup, but also when the app is killed and resumed which results in 2 explore fragments
if (addExploreFragment){
fragmentTransaction.replace(R.id.content_frame, mExploreFragment, EXPLORE_FRAGMENT_TAG);
Log.d(tag, "Replaced frame and added "+ mFragmentTags[position]);
} else {
//add fragment for the first time
if (getSupportFragmentManager().findFragmentByTag(mFragmentTags[position]) == null && position != 0) {
fragmentTransaction.add(R.id.content_frame, mFragments[position], mFragmentTags[position]);
Log.d(tag, "Added Fragment: "+ mFragmentTags[position]);
}
//shows and hides fragments
for (int i = 0; i < mFragments.length; i++) {
if (i == position) {
fragmentTransaction.show(mFragments[i]);
Log.d(tag, "Showing Fragment: "+ mFragmentTags[i]);
} else {
if (getSupportFragmentManager().findFragmentByTag(mFragmentTags[i]) != null) {
fragmentTransaction.hide(mFragments[i]);
Log.d(tag, "Hid Fragment: "+ mFragmentTags[i]);
}
}
}
}
fragmentTransaction.commit();
//not null check for calling selectItem(0) before loading the drawer
if (mDrawerList != null){
mDrawerList.setItemChecked(position, true);
}
}
I know for sure, the explore fragment is getting created twice and the two instances behave independently of each other (just sharing).
I'm lost what to do next. This is an issue which can be reproduced very easily on low end devices but on a device like Nexus 4 (my test device), the issue can be reproduced by changing the device language.
Has anyone got any ideas about this? Basically if the addExploreFragment block doesn't get called when there is already an exploreFragment, this issue could be solved, I think, but I've been unable to do so. Also, I tried removing all the fragments and then adding the exploreFragment but same thing happens (50% of times).
Thanks! and sorry for the long post, I felt I should share all the details.
Update: When I change the device language and come back to the app on Activity 2 and go back to Home activity, it has the fragment B open which is good, but fragment A get recreated because it's a heavy fragment and the system probably removed it from memory. Again, that's ok that it gets recreated IF it got removed by the system but why does it get recreated when it's not removed. I believe it's something with my code, on every 2nd attempt (without closing the app) this happens, 2 instances of the heavy fragment A. Out of ideas.
But shouldn't fragmentTransaction.replace remove all the previously added fragments and then add exploreFragment. It's not working like that. Neither fragment A nor Fragment B are getting removed.
I found out something new and rather odd to me. When you use fragmentTransaction.add, the listeners you have, like DrawerItemClickListener, on the previous fragment, are still active. And this is even if you use fragmentTransaction.commit.
So...I suspect when the add method is used, you actually clicked on another hidden button or hidden UI that has an event listener on the previous fragment. I don't like this of course and the effect may be very confusing. Yes, this happened to me and I didn't understand why for a while.
For now, I think the easiest code fix would be to use the replace method instead of add. The replace() makes listeners inactive. If it works, then you can make a better/elegant fix.
Let me know what happens....
I started to notice your post
when I go to fragment B and go to another activity
When you interact or start another Activity, you start a new set of Fragments. Look at this Google webpage # Fragments Lifecycle.
For clarification of my claim, there is a quote saying
A fragment must always be embedded in an activity and the fragment's
lifecycle is directly affected by the host activity's lifecycle.
You might as well read few paragraphs of it, at least.
I am not sure what your solution should be. Perhaps make the fragments distinctive, different and clear between the two Activities you have.

Categories

Resources