I'ms studying fragments and I checked the tutorial in the docs here with the articles master details example. We have 2 fragments one for article titles and when selected the detailed article view appears (Multi-pane layout). I get most of the tutorial except one small part, why they check the savedInstancestate inside the onCreate method.
so my question is about the onCreate() method of the container activity. It has this check
if (savedInstanceState != null) {
return;
}
When I remove this, the fragments are overlapped in the ui. so I know it prevents this but I don't know why ? I want some one explain this to me.
Thanks in advance.
Edit: The full Method
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.news_articles);
// Check whether the activity is using the layout version with
// the fragment_container FrameLayout. If so, we must add the first fragment
if (findViewById(R.id.fragment_container) != null) {
// However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else
// we could end up with overlapping fragments.
if (savedInstanceState != null) {
return;
}
// Create an instance of ExampleFragment
HeadlinesFragment firstFragment = new HeadlinesFragment();
// In case this activity was started with special instructions from an Intent,
// pass the Intent's extras to the fragment as arguments
firstFragment.setArguments(getIntent().getExtras());
// Add the fragment to the 'fragment_container' FrameLayout
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, firstFragment).commit();
}
}
I got it.
The point is that: the activity's contained fragments are saved automatically if the activity is destroyed by the screen rotation behavior.
So when the activity is restored from previous state (screen rotation) the onCreate() method is called again, that means the fragment will be added again when screen rotated (according to the code above). so we have to check inside the onCreate() method if we are restored from rotation if (savedInstanceState != null) so no need to re-add the fragment, just do nothing.
savedInstanceState checks for the last saved state.
In android whenever you rotate your device or came back from another Activity , Android's general life cycle starts as it should, like onCreate>onStart>onResume and so on..
Which means your whole activity is being started from fresh.
But in savedInstanceState , you will get the last state of your UI , that you had saved or you were using.
Related
I got 2 activities, A and B. Each activity is a container for fragments which are replaced with a FragmentTransaction.
I got an issue on some devices that when a user opens Activity B while he was in Activity A, the first activity is probably destroyed, which means that when a user clicks the back button, it makes the first activity recreated while in a normal device, it would just resume.
My main issue is that the user loses its fragment stack he had in the first activity. When the user opened the 2nd activity, he was already 3 fragments "deep" the first activity. How can I restore the stack and return the user to the point he's been before the first activity was destroyed?
This should be handled by the Android OS automatically. You can turn developer option "don't keep activities" on to always mimic this behavior (destroying your activity) when your activity goes to the background. After that you can start debugging. Some things to check:
In onCreate of the activity, are you calling the super onCreate with
the savedInstanceState?
If you put a breakpoint at the start of onCreate, when you "come
back" to the activity, is there a saved instance state?
Where are you creating the fragments? Are you re-creating them
manually (you shouldn't)?
Are your fragments hardcoded in the layout or replaced in the layout
(replacing a container view)?
* EDIT *
From your reply I derive that this is the problem, you say: "In the end of the onCreate I am replacing the fragment with a fragment transaction and thus load the first fragment of the app" => you should not do that when the savedInstanceState is not null. Otherwise you're destroying what is already there from the saved state.
Check here: https://developer.android.com/training/basics/fragments/fragment-ui.html
Notice the return when savedInstanceState is not null.
public class MainActivity extends FragmentActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.news_articles);
// Check that the activity is using the layout version with
// the fragment_container FrameLayout
if (findViewById(R.id.fragment_container) != null) {
// However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else
// we could end up with overlapping fragments.
if (savedInstanceState != null) {
return;
}
// Create a new Fragment to be placed in the activity layout
HeadlinesFragment firstFragment = new HeadlinesFragment();
// In case this activity was started with special instructions from an
// Intent, pass the Intent's extras to the fragment as arguments
firstFragment.setArguments(getIntent().getExtras());
// Add the fragment to the 'fragment_container' FrameLayout
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, firstFragment).commit();
}
}
}
I want to be able to define a listener (an Activity, Fragment, etc) and be able to pass it through any number of nested Fragments before I decide to finally invoke the callback. That way it can call callback.someFunction() and it doesn't need to know what Activity or Fragment that callback is attached to.
Right now it seems, though, that there is no good way to send a listener through a bunch of Fragments. I initially considered passing it through the constructors, but then the listener reference would be nulled out on a configuration change like a screen rotation.
I then considered the onAttach() methods, but these only give you access to the context of the base Activity which doesn't necessarily do what I want, either.
I also considered passing the listeners in through newInstance() (which is normally how you save arguments passed into Fragments because the contents of getArguments() survives configuration changes via the Bundle), but I could not see any good way to save the listener in the argument Bundle.
What can I do?
What you should do is recreate everything when a configuration change happens or when the activity is re-created and its instance state restored...which is the same for that matter...
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.acitivyt_layout);
if(savedInstanceState != null){
//1. restore instance state here
savedInstanceState.getString("whatever");
//2. try to find the fragment
Fragment f = getSupportFragmentManager().findFragmentByTag("FRAGMENT TAG");
//3. make sure the fragment is correctly set up if already loaded
if(f != null){
((ConcreteFragmentType)f).setListener(your_listener);
//bring the fragment to the front or show it using the fragment manager
}
//4. or else add a brand-new instance
else{
ConcreteFragmentType c = ConcreteFragmentType.newInstance();
c.setListener(your_listener);
getSupportFragmentManager().beginTransaction()
.add(R.id.fragmentContainer, c)
.commit();
}
}
}
I'm having an issue of having two instances of the same fragment being attached to the activity. I have ActivityA attaching FragmentA on onCreate. When I leave the app while being on this Activity, browse other apps for a while, and return to the app, I see that the system is trying to re-create the activity. My log shows the code from the Fragment being ran TWICE. My guess is the Fragment is already attached but then the Activity attempts to create a new instance of FragmentA.
What happens to the Activity/Fragment when the system removes them from memory, and what's the best way to handle this? Any links would be helpful.
Will provide code if needed.
The best way to handle this is to check in your onCreate() method if your activity if being recreated from a previous state or not. I'm assuming you add your fragment on the onCreate() method of your activity. You can do something like this:
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
if (savedInstanceState == null)
{
// Add the fragment here to your activity
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.content, new YourFragment());
ft.commit();
}
}
By doing this, you are basically saying that if a previous state is not found, you add your fragment. Otherwise you automatically get back the fragment that already exists.
I'm currently dealing with an issue with Android & It's Re-Creation Cycle on screen rotation:
I have one single Activity and lots of Fragments (Support-V4) within.
For example, the Login it's on a Single Activity with a Fragment, when the logs-in then the App changes it's navigation behavior and uses multiple fragments, I did this, because passing data between Fragment A to Fragment B it's way much easier than passing data Between an Activity A to an Activity B.
So My issue it's presented when I rotate the device, on my first approach, the initial fragment was loaded, but what would happen, if the user it's on Page 15 and it rotates it's device, it would return to Fragment 1 and give a very bad user-experience. I set all my fragments to retain their instance and added this on the MainActivity on Create:
#Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.main_layout);
initBackStackManager();
initControllers();
mayDownloadData();
setTitle();
if(savedInstanceState == null){
addAreaFragment();
}
}
Now, the first fragment is not loaded after screen orientation change, but If I try to make a fragment transaction, it says Can not perform FragmentTransaction.commit() after onSaveInstanceState(), is there a way to handle this? Or Do I really really need to use multiple Activities with a Fragment embedded within?
Thank you very much!
EDITED
I forgot to add that this happens only on a specific Fragment... For example I have the following fragment flow:
AreaFragment -> WaiterSelectionFragment -> WaiterOptionsFragment.
If I'm in the AreaFragment and I rotate the device I can still add/replace fragments and nothing happens, no error it's being thrown. If I'm on the WaiterSelectionFragment no error happens too. BUT, If I'm on the WaiterOptionsFragment the error it's being thrown. The WaiterSelectionFragment has the following structure:
LinearLayout
FragmentTabHost
Inside the FragmentTabHost there are some fragments, and that's where the error it's happening. You might wonder Why FragmentTabHost? easy, the Customer wants that App to show the TabBar, If I use Native Android Tabs the Tabs get rearranged to the ActionBar when on
Landscape position.
EDIT 2
I've used the method provided by #AJ Macdonald, but no luck so far.
I have my Current Fragment being saved at onSaveInstanceState(Bundle) method and restore my fragment on onRestoreInstanceState(Bundle) method on the Android Activity, I recover my back button and the current Fragment but when I get to the third Fragment the error still occurs. I'm using a ViewPager that holds 4 Fragments, Will this be causing the Issue? Only on this section of the App Happens. I've 4 (main workflow) fragments, on the First, Second and Third Fragment no error it's being presented, only on the ViewPager part.
Give each of your fragments a unique tag.
In your activity's onSaveInstanceState, store the current fragment. (This will probably be easiest to do if you keep a variable that automatically updates every time the fragment changes.)
In your activity's onCreate or onRestoreInstanceState, pull the tag out of the saved bundle and start a new fragment of that type.
public static final int FRAGMENT_A = 0;
public static final int FRAGMENT_B = 1;
private int currentFragment;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//other stuff
if(savedInstanceState == null){
addAreaFragment();
currentFragment = FRAGMENT_A;
}else{
currentFragment = savedInstanceState.getInt("currentFragment");
switch(currentFragment){
case FRAGMENT_A:
addAreaFragment();
break;
case FRAGMENT_B:
addFragmentB();
}
}
}
// when you switch fragment A for fragment B:
currentFragment = FRAGMENT_B;
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putInt("currentFragment", currentFragment);
super.onSaveInstanceState(savedInstanceState);
}
A suggestion to try is to use FragmentTransaction.commitAllowingStateLoss() in place of FragmentTransaction.commit(). That should stop the Exception from being thrown, but the downside is if you rotate the device again the most recent state of the UI may not return. That is a suggestion given that I am not sure of the effect of using FragmentTabHost, if it has any effect at all.
I have the following setup which is quite common: in landscape mode I have 2 fragments - A and B. In portrait mode I have only fragment A. I tried to detect if I am in second setup mode by just a simple check:
getSupportFragmentManager().findFragmentById(R.id.frag_b) == null
This was working fine until I was in 2 fragment mode and rotating the device to 1 fragment mode - after that the manager was finding the fragment B and not returning null. I believe the fragment manager was somehow saving and loading its state from previous setup. The first question - why is this working this way and what can I do with it?
Second question - I tried to remove the fragment but was not able to do that. Here how I tried:
Fragment f = manager.findFragmentById(R.id.frag_b);
manager.beginTransaction().remove(f).commit();
f = manager.findFragmentById(R.id.frag_b); // still there
I guess remove() didn't work since it was not added using add() but rather loaded from previous state xml. Is there a way to remove a fragment from manager in this case?
P.S. The solution for me will be to have another way of detection in which mode I am. I already have this, just need to know how it works for better understanding of fragments and their behavior.
you can detect if you are on portrait or landscape with this:
getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
Being new to fragments and after struggling a day I think I understood most of the logic behind fragments and their usage. The fact that fragment manager shows fragments other than the ones defined for current orientation is not a bug, it's a feature. Here are some observations summarized:
When changing configuration, the FragmentManager saves all the fragments it has currently and loads them back so they are ready in onCreate() method of container activity. This means that if you have fragment A and B in some layout and you rotate the device to a state where only A should be - you still will find B in FragmentManager. It might be not added (check with Fragment.isAdded()) or might be added to some other container, which is not visible now, but its there. This is quite useful since B saves its state (given that you did it properly in B's life cycle functions) and you don't have to take care of it on activity level. Maybe, at some point in future, you would like to dynamically add fragment B to your UI and it will have all its state saved from previous configuration.
Related to the second question above - fragments that need to be moved from container to container should not be declared in xml, they should be added dynamically. Otherwise you will not be able to change its container and will get IllegalStateException: Can't change container ID of fragment exception. Instead, you define containers in XML file and give them an ID, for example:
<RelativeLayout
android:id="#+id/fragmentContainer"
android:layout_height="match_parent"
android:layout_width="0dp"
android:layout_weight="0.7" />
and later add to it using something like
FragmentManager.beginTransaction().add(R.id.fragmentContainer, fragment);
If you need to use some fragment, first look if FragmentManager has it - if yes, then just reuse it - it will have its state saved as a bonus. To look for fragment:
private void MyFragment getMyFragment() {
List<Fragment> fragments = getSupportFragmentManager().getFragments();
if (fragments != null) {
for (Fragment f : fragments) {
// an example of search criteria
if (f instanceof MyFragment) {
return (MyFragment) f;
}
}
}
return null;
}
if it is null then create a new one, otherwise you can go ahead and reuse it, if needed.
One way of reusing a fragment is putting it in another container. You will need some effort in order not to get IllegalStateException: Can't change container ID of fragment. Here is the code I used with some comments to help understand it:
private void moveFragment(MyFragment frag) {
int targetContainer = R.id.myContainerLandscape;
// first check if it is added to a correct place
if (frag.isAdded()) {
View v = frag.getView();
if (v != null) {
int id = ((ViewGroup) v.getParent()).getId();
if (id == targetContainer) {
// already added to correct container, skip
return;
}
}
}
FragmentManager manager = getSupportFragmentManager();
// Remove the fragment from its previous container first (done
// here without check if added or nor, check if needed).
// In order not to get 'Can't change container ID...' exception
// we need to assure several things:
// 1. its not hardcoded in xml - you can remove()
// fragment only if you have add()-ed before
// 2. if this fragment is sitting deep in a backstack
// then you will still get the above mentioned exception.
// If the stack is fragA-fragB-fragC <-top then you get
// exception on moving fragment A. Need to clean the back
// stack first. HOWEVER, note that in that case you will
// lose the fragB and fragC together with their states!
// For that reason save them first - I will assume there is
// only one on top of current fragment to make code simpler.
// before cleaning save the top fragment so that it is not destroyed
OtherFragment temp = getOtherFragment(); // use function 'getMyFragment()' above
// clean the backstack
manager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
// remove the fragment from its current container
manager.beginTransaction().remove(frag).commit();
// call executePendingTransactions() for your changes to be available
// right after this call, otherwise the previous 'commit()' just submits
// the task to main thread and it will be done somewhere in the future
manager.executePendingTransactions();
if (getOtherFragment() == null && temp != null) {
// Add the fragment we wanted to 'save' back to manager, this
// time without any relation to backstack or container. Later
// we will be able to find it using getOtherFragment() and the
// fragment manager will be able to save/load its state for us.
manager.beginTransaction().add(temp, null).commit();
manager.executePendingTransactions();
}
// now add our fragment
FragmentTransaction transaction = manager.beginTransaction();
transaction.replace(targetContainer, frag);
transaction.commit();
manager.executePendingTransactions();
}
This was the result of my first day of dealing with fragments seriously, at least my task was accomplished in my app. Would be good to get some comments from experienced guys on what is wrong here and what can be improved.