My Navigation graph has two destinations, a Fragment and a DialogFragment. The Fragment contains a Button that navigates to the DialogFragment when pressed.
Everything works as expected, except if I click the button very quickly. Doing so can trigger a
IllegalArgumentException: navigation destination
com.example.app:id/show_dialog is unknown to this NavController
To fix this, I ensure that the current destination is the Fragment containing the show_dialog action:
val navController = findNavController()
val currentDest = navController.currentDestination?.id
if (currentDest == R.id.test_fragment) {
navController.navigate(TestFragmentDirections.showDialog())
}
Making this change appears to fix the issue. However, I would like to know:
Why is it necessary to wrap the navigate call with a conditional statement in this situation?
Your question is just based on the Android inside Architecture and is also dependent on the hardware performance. Just wrap it in a try/catch block:
try{
findNavController().navigate(TestFragmentDirections.showDialog())
}catch(e: IllegalArgumentException){
e.printStackTrace
}
You might be getting IllegalArgumentException because if you see showDialog() method, you will find implementation as below:
public void show(FragmentManager manager, String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
}
I'll suggest to write your own showDialog menthod with below implementation:
fragmentManager.beginTransaction()
.add(dialog, "TAG")
.commitAllowingStateLoss();
Related
I have an application with 4 fragments: MainFragment, ActionFragment, DoneFragment, FailedFragment. When application launched it shows MainFragment. Than application receive some event and show ActionFragment with two buttons 'yes' and 'no'. If user press 'yes', applicaiton shows DoneFragment, otherwise FailedFragment. When user press one time to back button on ActionFragment, DoneFragment or FailedFragment application must show MainFragment.
Improtant: if ActionFragment, DoneFragment or FailedFragment already opened and some event is occure again, application should show ActionFragment fragment with new event data.
So, I need:
if ActionFragment, DoneFragment or FailedFragment already opened and event occur, I should replace top fragment with ActionFragment
Otherwise I should simply add ActionFragment.
I am trying:
fun addOrReplaceFragment(fragment: Fragment, tag: String) {
val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
val previous = fragmentManager.findFragmentByTag(tag)
if (previous == null) {
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE)
fragmentTransaction.add(R.id.main_fragment_container, fragment, tag)
fragmentTransaction.addToBackStack(tag)
} else {
fragmentManager.popBackStack(previous.id, 0)
fragmentTransaction.remove(previous)
fragmentTransaction.add(R.id.main_fragment_container, fragment, tag)
fragmentTransaction.addToBackStack(tag)
}
fragmentTransaction.commitAllowingStateLoss()
}
// ...
addOrReplaceFragment(ActionFragment(), "singleTag")
// ...
addOrReplaceFragment(DoneFragment(), "singleTag")
// ...
addOrReplaceFragment(FailedFragment(), "singleTag")
Here is popBackStack() doesn't work. When ActionFragment is opened, DoneFragment or FailedFragment just adding above. And user have to press back two times to get back to MainFragment.
I am find solution change popBackStack() to popBackStackImmediate(). It works well, but if activity is minimized it produce crash with IllegalStateException, because popBackStackImmediate() cannot be called after onSaveInstanceState().
How to replace top fragment and avoid IllegalStateException?
Try this :
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.FragmentToBeReplaced,theFragmentToBeAdded);
ft.commit();
https://developer.android.com/reference/android/app/FragmentManager.html#isStateSaved()
Use isStateSaved to avoid losing state when a transaction happened.
And I think Android navigation component might be easier way to navigate between fragments.
My application has a Fragment inside its Activity. I would like to programmatically replace the fragment by another one from the current fragment itself.
For example, if I click on a button inside the fragment, the fragment should be replaced with another one, but the activity should remain the same.
Is it possible? If so, how to do it?
It's actually easy to call the activity to replace the fragment.
You need to cast getActivity():
((MyActivity) getActivity())
Then you can call methods from MyActivity, for example:
((MyActivity) getActivity()).replaceFragments(Object... params);
Of course, this assumes you have a replaceFragments() method in your activity that handles the fragment replace process.
Edit: #ismailarilik added the possible code of replaceFragments in this code with the first comment below which was written by #silva96:
The code of replaceFragments could be:
public void replaceFragments(Class fragmentClass) {
Fragment fragment = null;
try {
fragment = (Fragment) fragmentClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
// Insert the fragment by replacing any existing fragment
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction().replace(R.id.flContent, fragment)
.commit();
}
from the official docs:
// Create new fragment and transaction
Fragment newFragment = new ExampleFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();
// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);
// Commit the transaction
transaction.commit();
In this example, newFragment replaces whatever fragment (if any) is currently in the layout container identified by the R.id.fragment_container ID. By calling addToBackStack(), the replaced fragment is saved to the back stack so the user can reverse the transaction and bring back the previous fragment by pressing the Back button.
The behavior you have described is exactly what fragments are designed to do. Please go through the official guide for a thorough understanding of fragments which will clear up all your questions.
http://developer.android.com/guide/components/fragments.html
Please note that fragment should NOT directly replace itself or any other fragments. Fragments should be separate entities. What fragment should do is to notify its parent activity that some event has happened. But it is, again, NOT a fragment job to decide what to do with that! It should be activity to decide to i.e. replace the fragment on phone, but to i.e. add another to existing one on tablets. So you are basically doing something wrong by design.
And, as others already mentioned, your activity should use FragmentManager ("native" or from compatibility library) to do the job (like replace() or add() or remove()):
http://developer.android.com/guide/components/fragments.html
Just as Marcin said, you shouldn't have a fragment start another fragment or activity. A better way to handle this situation is by creating a callback implementation for the main activity to handle requests such as start a new fragment. Here is a great example in the android developer guide.
There is a way which works; Just (in the fragment) do the following:
getFragmentManager().beginTransaction()
.add(R.id. container_of_this_frag, new MyNewFragment())
.remove(this)
.commit();
When using nested fragments, we don't want every inner fragment replacement goes to the outer most activity. A mechanism allowing a fragment to notify its parent that it wants to change to another fragment can be useful.
Here is my code in Kotlin, I think it is easy to translate into java.
interface FragmentNavigator {
fun navigateTo(fragment: Fragment)
}
class NavigableFragment: Fragment() {
var navigator: FragmentNavigator? = null
override fun onDetach() {
super.onDetach()
navigator = null
}
}
Inner fragments need to extend NavigableFragment, and use following code to change itself to another fragment.
navigator?.navigateTo(anotherFragment)
Outer activities or fragments need to implement FragmentNavigator, and override navigateTo.
override fun navigateTo(fragment: Fragment) {
supportFragmentManager.beginTransaction().replace(view_id, fragment).commit()
}
//Use childFragmentManager instead of supportFragmentManager a fragment
Finally in outer activities or fragments, override onAttachFragment
override fun onAttachFragment(fragment: Fragment?) {
super.onAttachFragment(fragment)
if(fragment is NavigableFragment) {
fragment.navigator = this
}
}
This worked for me:
getActivity().getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container,
new MenuFragment()).commit();
For Kotlin.
(activity as YourActivityLauncherFragment)
.supportFragmentManager.beginTransaction()
.replace(R.id.yourFragmentContainer, YourFragmentName()).setReorderingAllowed(true)
.commit()
I am using drawer menu in my app.I select one option from menu and open fragment and from that fragment call an Activity.Since here it is working fine but when I press back button(OnbackPress) then app is crashed.
bellow is the error.
"Unable start activity...ClassCastException...cannot be cast to Home_Tab"
This is MainActivity code.
if (savedInstanceState == null) {
homefragment = Home_tab()
fragmentTransaction = fragmentManager!!.beginTransaction()
fragmentTransaction!!.replace(R.id.frame, homefragment)
fragmentTransaction!!.addToBackStack(null)
fragmentTransaction!!.commit()
} else {
homefragment = supportFragmentManager.fragments[0] as Home_tab //Crash at this line
}
Code from where backPress Called.
override fun onBackPressed() {
super.onBackPressed()
finish()
}
//Add a check like this before casting.
//It is a smart cast and you can directly use the result.
Fragment fragmentZero = supportFragmentManager.fragments[0]
if (fragmentZero is Home_tab) {
//Casting is done, you can directly use fragment here
homefragment = fragmentZero
}
Well based on your code you add the fragment to the fragmentmanager (regular one) but try to get it back from the supportfragmentmanager. Those are two different classes and your fragment can only extend one
You're are mixin fragment manager et support fragment manager, i'll go with suppport one since this is the right way to do it. To get current display fragment added with a container ID use findFragmentById
if (savedInstanceState == null) {
homefragment = Home_tab()
supportFragmentManager?.let{
fragmentTransaction = it.beginTransaction()
fragmentTransaction.replace(R.id.frame, homefragment)
fragmentTransaction.addToBackStack(null)
fragmentTransaction.commit()
}
} else {
homefragment = supportFragmentManager.findFragmentById(R.id.frame) as Home_tab
}
My application has a Fragment inside its Activity. I would like to programmatically replace the fragment by another one from the current fragment itself.
For example, if I click on a button inside the fragment, the fragment should be replaced with another one, but the activity should remain the same.
Is it possible? If so, how to do it?
It's actually easy to call the activity to replace the fragment.
You need to cast getActivity():
((MyActivity) getActivity())
Then you can call methods from MyActivity, for example:
((MyActivity) getActivity()).replaceFragments(Object... params);
Of course, this assumes you have a replaceFragments() method in your activity that handles the fragment replace process.
Edit: #ismailarilik added the possible code of replaceFragments in this code with the first comment below which was written by #silva96:
The code of replaceFragments could be:
public void replaceFragments(Class fragmentClass) {
Fragment fragment = null;
try {
fragment = (Fragment) fragmentClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
// Insert the fragment by replacing any existing fragment
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction().replace(R.id.flContent, fragment)
.commit();
}
from the official docs:
// Create new fragment and transaction
Fragment newFragment = new ExampleFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();
// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);
// Commit the transaction
transaction.commit();
In this example, newFragment replaces whatever fragment (if any) is currently in the layout container identified by the R.id.fragment_container ID. By calling addToBackStack(), the replaced fragment is saved to the back stack so the user can reverse the transaction and bring back the previous fragment by pressing the Back button.
The behavior you have described is exactly what fragments are designed to do. Please go through the official guide for a thorough understanding of fragments which will clear up all your questions.
http://developer.android.com/guide/components/fragments.html
Please note that fragment should NOT directly replace itself or any other fragments. Fragments should be separate entities. What fragment should do is to notify its parent activity that some event has happened. But it is, again, NOT a fragment job to decide what to do with that! It should be activity to decide to i.e. replace the fragment on phone, but to i.e. add another to existing one on tablets. So you are basically doing something wrong by design.
And, as others already mentioned, your activity should use FragmentManager ("native" or from compatibility library) to do the job (like replace() or add() or remove()):
http://developer.android.com/guide/components/fragments.html
Just as Marcin said, you shouldn't have a fragment start another fragment or activity. A better way to handle this situation is by creating a callback implementation for the main activity to handle requests such as start a new fragment. Here is a great example in the android developer guide.
There is a way which works; Just (in the fragment) do the following:
getFragmentManager().beginTransaction()
.add(R.id. container_of_this_frag, new MyNewFragment())
.remove(this)
.commit();
When using nested fragments, we don't want every inner fragment replacement goes to the outer most activity. A mechanism allowing a fragment to notify its parent that it wants to change to another fragment can be useful.
Here is my code in Kotlin, I think it is easy to translate into java.
interface FragmentNavigator {
fun navigateTo(fragment: Fragment)
}
class NavigableFragment: Fragment() {
var navigator: FragmentNavigator? = null
override fun onDetach() {
super.onDetach()
navigator = null
}
}
Inner fragments need to extend NavigableFragment, and use following code to change itself to another fragment.
navigator?.navigateTo(anotherFragment)
Outer activities or fragments need to implement FragmentNavigator, and override navigateTo.
override fun navigateTo(fragment: Fragment) {
supportFragmentManager.beginTransaction().replace(view_id, fragment).commit()
}
//Use childFragmentManager instead of supportFragmentManager a fragment
Finally in outer activities or fragments, override onAttachFragment
override fun onAttachFragment(fragment: Fragment?) {
super.onAttachFragment(fragment)
if(fragment is NavigableFragment) {
fragment.navigator = this
}
}
This worked for me:
getActivity().getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container,
new MenuFragment()).commit();
For Kotlin.
(activity as YourActivityLauncherFragment)
.supportFragmentManager.beginTransaction()
.replace(R.id.yourFragmentContainer, YourFragmentName()).setReorderingAllowed(true)
.commit()
Since Tab Activity is deprecated I'm trying to implement tabs with fragments. As you can see in the lots of StackOverFlow questions back stack is an issue when you work with fragments and which has its own back stack.
So the thing I'm trying to do is, there is a fragment in each tab and this fragment can call another fragment within the same tab and its also the same for the other tabs.
Since there is only one activity then there is only one back stack for whole application. So I need to create my custom back stack which is separated for each tab. It's also the same common idea in the other questions. I need to find a way to create custom back stack but I couldnt find any example to take a look.
Is there any tutorial or any example piece of code doing something similar ? Thanks in advance.
There is a backstack for the whole application, but there is also a backstack for fragments.
Perhaps have a read of this:
http://developer.android.com/guide/components/fragments.html#Transactions
When you perform a fragment transaction (add, replace or remove), you can add that transaction to the backstack.
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.fragmentContainer, fragment);
ft.addToBackStack(null);
ft.commit();
And now when you press back, the latest fragment will be 'popped' from the fragment backstack.
You could also override the onBackPressed(), and manage your fragment backstack there. (I'm currently having trouble trying to work out how to do this efficiently).
Anyway, there are several calls available from the FragmentManager to do with the backstack. The most useful to me being:
FragmentManager.getBackStackEntryCount()
and
FragmentManager.PopBackStack()
Sorry for late answer, but this might help somebody. I have added functionality like this in my project. I used fragment tabhost to add backstacks within it. Main logic will remain same in others.
Basically I took,
Stack<String> fragmentStack = new Stack<>();
boolean bBackPress = false;
String strPrevTab;
FragmentTabHost tabHost;
strPrevTab = "tag"; // Add tag for tab which is selected by default
tabHost.setOnTabChangedListener( new TabHost.OnTabChangeListener() {
#Override
public void onTabChanged( String tabId ) {
if ( !bBackPress ) {
if ( fragmentStack.contains( tabId ) ) {
fragmentStack.remove( tabId );
}
fragmentStack.push( strPrevTab );
strPrevTab = tabId;
}
}
} );
#Override
public void onBackPressed() {
if ( fragmentStack.size() == 0 ) {
finish();
} else {
strPrevTab = fragmentStack.pop();
bBackPress = true;
tabHost.setCurrentTabByTag( strPrevTab );
bBackPress = false;
}
}
Hope this helps!