Fragment being reset after configuration change - android

I'm currently creating an app using a bluetooth connection. The connection is handled inside an simple object (HandleConnection). When the connection is made, my app goes into "remote" mode, which means I'm replacing my main fragment with another one (RemoteFragment) which use the "HandleConnection" object.
Until the everything is going well, since at the end of my "onCreate" (inside my activity) I set the RemoteFragment's handleConnection attribute:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_layout);
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
if (savedInstanceState == null) {
retainedFragment = new RetainedFragment();
getFragmentManager().beginTransaction().add(retainedFragment, RetainedFragment.class.toString()).commit();
handleConnection = new HandleConnection(this, handler);
retainedFragment.setHandleConnection(handleConnection);
} else {
remoteFragment = (RemoteFragment) getFragmentManager().findFragmentByTag(RemoteFragment.class.toString());
retainedFragment = (RetainedFragment) getFragmentManager().findFragmentByTag(RetainedFragment.class.toString());
if (remoteFragment == null)
remoteFragment = new RemoteFragment();
handleConnection = retainedFragment.getHandleConnection();
remoteFragment.setHandleConnection(handleConnection);
}
fragmentTransaction.commit();
}
Everything works fine except when I turn into landscape mode.
Then (after playing in debug mode), it seems my RemoteFragment is recreated after my setter is called, which obviously mean my handleConnection attribute is null.
Why is it doing that ? I would understand if I didn't reuse the previous fragment (then I would have two different fragments on top of each other) but in this case it makes non sense.
I made a workaround by calling a callback function inside the onCreateView to get the HandleConnection object, but why would the attribute be null when I called a setter right before ?
Thanks !

A Fragment's life cycle is directly affected by the host activity's life cycle. when the activity is destroyed, so are all fragments. During Layout changes, an activity is destroyed and recreated.
In this case, after rotation, your else condition in the onCreate() method will execute and hence the behavior that you are getting. Have a look at the documentation for thorough understanding.
http://developer.android.com/guide/components/fragments.html

You need to add to your activity registration in AndroidManifes.xml something like that: <activity android:configChanges="orientation|screenSize"/>
Read this article

So apparently my problem was that I wasn't using the right instance of my RemoteFragment, which means my current reference (in my activity) had the correct non-null value for my object, but the fragment displayed wasn't this one (it was the old one which was recreated and thus had a null value for the handleConnection).

Related

Unknown fragment after rotation

In my application I create a fragment with the keyword new and set it by FragmentTransaction.
Upon rotation a stumbled upon a NullPointerException in the method onActivityCreated() indicating a missing injection, that I do after the call to new. I suspected the fragment was not created by my code und proved this by logging the hashCode(). It looks like a fragment is created automatically by the system upon rotation.
Where does it come from?
Is it created by the fragment manager?
How am I supposed to use it correctly?
How can I access it, to set the missing value?
For now I ignore it by testing for the null value, in which case onActivityCreated() does nothing. Instead use the fragment I create with new. However, this does not feel very satisfying, to throw away an object, that was already created.
Where does it come from? Is it created by the fragment manager?
On Activity recreation, Android will restore the fragments which already exist in activity's fragments manager
How am I supposed to use it correctly?
public void onCreate(Bundle savedInstanceState){
if(savedInstanceState == null){
//activity is created for first time
//commit the fragment
}else{
//Activity is recreated(by means of rotation or something else)
//Dont commit the fragment, fragmet will be restored by the system
}
}
How can I access it, to set the missing value?
Normally, you have to handle this inside the fragment using onSaveInstanceState method. You can get the fragment instance by using, getSupportFragmentManager.findFragmentById(R.id.container) or getSupportFragmentManager.findFragmentByTag(tagName)

Passing a listener through a chain of Fragments?

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();
}
}
}

When will a dynamically added fragment be re-added?

The Fragment documentation shows an example of an activity dynamically adding a fragment in onCreate(...):
if (savedInstanceState == null) {
// During initial setup, plug in the details fragment.
DetailsFragment details = new DetailsFragment();
details.setArguments(getIntent().getExtras());
getFragmentManager().beginTransaction().add(android.R.id.content, details).commit();
}
This almost makes sense to me, except for one detail. I thought the reason for checking savedInstanceState == null was that if the activity is being re-created, we can expect the framework to re-add the fragment for us. However, I thought the framework would only do this if the fragment has a tag, and the example uses the version of FragmentTransaction#add(...) that does not take a tag. So as I understand it, if this activity were recreated, it would not have a DetailsFragment.
Is my understanding wrong? And if the framework does re-add the fragment, at what point in the activity's lifecycle is it guaranteed to have done so?
I thought the framework would only do this [re-add the fragment] if the fragment has a tag
No. According to the documentation in "Adding a fragment to an activity" section:
each fragment requires a unique identifier that the system can use to restore the fragment if the activity is restarted.
Thus, you have 3 possibilities to handle this: using an unique id, unique tag or none of them. Yes, neither of the previous two requirements.The purpose of adding a tag is to capture the fragment. This is useful if you want to retrieve it easily and play with it (such as performing transactions). However, it's not required.
When you use add(int, Fragment), it calls add(int, Fragment, String) with a null tag at the 3rd parameter. Therefore, the system will use the ID of the container view instead (1st param in add()). Thus, the fragment is restored without any id or tag that you supplied but correctly handled by the system.
Note: in reference of add(int, Fragment, String), you can see this quote: "Optional tag name for the fragment, to later retrieve the fragment with FragmentManager.findFragmentByTag(String)" - How could it be optional if we need it to restore fragments? ;)
At what point in the activity's lifecycle is it guaranteed to have done so?
I cannot honestly respond on it, maybe someone has the right answer but, here's what I think.
As you can see in "Handling configuration changes", when orientation (or any specific change) occurs on activity, it calls onDestroy() and directly onCreate(). At the point of (activity's) onCreate() is called, the child fragment will receive a callback in onAttach(). Beside, when the activity has received its onCreate() callback, a child fragment receives automatically a onActivityCreated() callback. After that, each method in activity will trigger the "exact" same child methods (onStart(), onResume(), onPause(), onStop()).
Thus, I think the safe point is onStart(), because activity triggers directly the call of the fragment's method, you're sure that the fragment is attached to it and you can handle onRestart() -> onStart() to update the UI.
I don't use tags with my Fragments and mine work just fine, I have the exact same test as you.
if (savedInstanceState == null) {
//fragment needs to be created
}
if (savedInstanceState != null) {
//fragment will automatically be there with no code
}

How can I avoid an IllegalStateException if I need to add a Fragment in onNewIntent() or after a run-time change?

I'm well aware of what an IllegalStateException is and why it happens when you are trying to commit FragmentTransactions after instance state has been saved. I've read through most of the popular Stackoverflow questions on the subject as well as "The Blog Post."
I have a particular scenario in which I've created a Fragment to display a custom progress spinner while the app is waiting for some search results to come in. So my search Activity is running in singleTop mode and relies on onNewIntent() to perform the search and display the spinner. This all works fine, but fails when run-time changes come in (like orientation changes). I'll get an IllegalStateException if I try removing the progress spinner Fragment after an orientation change, or when adding the spinner after a voice search. My setup looks like this:
#Override
protected void onNewIntent(Intent intent) {
if(intent.getAction().equals(Intent.ACTION_SEARCH)) {
performSearch(intent.getStringExtra(SearchManager.QUERY));
}
}
The performSearch() method sends the request to my server and then adds the progress spinner Fragment like this...
private void showProgressSpinner() {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.setCustomAnimations(R.anim.fragment_fade_in, R.anim.fragment_fade_out);
ProgressFragment spinner = ProgressFragment.newInstance();
transaction.add(R.id.searchContainer, spinner, "spinner");
transaction.commit();
}
Then when the search results come in through an asynchronous callback, I remove the progress spinner Fragment like this...
private void dismissProgressSpinner() {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.setCustomAnimations(R.anim.fragment_fade_in, R.anim.fragment_fade_out);
ProgressFragment spinner = (ProgressFragment) fragmentManager.findFragmentByTag("spinner");
if(spinner != null) {
transaction.remove(spinner);
}
transaction.commit();
}
If an orientation change comes in while the progress spinner Fragment is being displayed, I get the IllegalStateException when the search results return and I try to remove the spinner Fragment. According to Alex Lockwood, putting the FragmentTransaction in onPostResume() can help, since you are guaranteed to have the state restored by then. This indeed works, but I need to remove the Fragment in my asynchronous callback from the search results, not when the Activity resumes.
So my question is, how can I commit this Fragment transaction after a state change, but within my async callback? I've tried using commitAllowingStateLoss() but I still get the exception because my spinner Fragment is still referencing the old destroyed Activity. What's the best way to handle this?
I've tried using commitAllowingStateLoss() but I still get the exception because my spinner Fragment is still referencing the old destroyed Activity.
your fragment was recreated after config change, ie. after user have rotated screen - your spinner fragment will be destroyed and recreated, and after onAttach it will reference new activity instance. Also all of this process is done by android on UI thread in single message, so there is no chance that your async operation callback (which should execute also on UI thread) gets executed in the middle.
I assume here you are not creating some local reference to activity inside ProgressFragment.
You could write additional logic that would make sure your commit is called in valid moment. ie. in your activity onStart set some static boolean allowCommit to true, and in onPause set it to false. Also add some static variable, searchWasFinished, and in your async callback check if allowCommit is true if so then immediately remove spinner, if not then only set searchWasFinished to true. Inside your Activity.onStart check if searchWasFinished==true and if so then remove fragment with commit. This is just an idea, probably more logic would have to be put in it.

reason to check for savedinstancestate on frgamentActivity oncreate()

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.

Categories

Resources