I am trying to get a better understanding of FragmentManager and FragmentTransactions to properly develop my application. It is specifically in regards to their lifecycle, and the long-term effect of committing a FragmentTransaction(add). The reason I have a confusion over it is when I ran a sample Activity, listed at the end of the post.
I purposely created a static FragmentManager variable called fragMan and initially set it to null. It is then checked against in onCreate() if it is null and when null value is seen, the fragMan variable is set to the getFragmentManager() return value. During a configuration change, the Log.d showed that fragma was not null, but the Fragment "CameraFragment" previously added was not found in fragman and the fragman.isDestroyed() returned true. This to me meant that the Activity.getFragmentManager() returns a new instance of a FragmentManager, and that the old FragmentManager instance in fragMan had its data wiped(?)
Here is where the confusion comes in.
1) How is "CameraFragment" still associated in the Activity on a configuration change and is found in
the new instance of FragmentManager?
2) When I hit the back button on my phone to exit the Activity, I then relaunched the sample
Activity using the Apps menu. The CameraFragment was not visible anymore, and the
onCreate() check revealed that fragMan was still not null. I thought that hitting the back button
called the default finish() command, clearing the Activity from memory and that restarting it
would produce the same result as the initial launch of the sample Activity?
Thank you for any and all help you can provide!
public class MainActivity extends Activity
{
static FragmentManager fragMan = null;
FragmentTransaction fragTransaction;
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (fragMan != null)
{
Log.d("log", Boolean.toString(fragMan.isDestroyed()));
if(fragMan.findFragmentByTag("Camera Preview") == null)
{
Log.d("log", "Camera Preview not found.");
}
}
else
{
fragMan = getFragmentManager();
fragTransaction = fragMan.beginTransaction();
Fragment cameraFragment = new CameraFragment();
ViewGroup root_view = (ViewGroup) findViewById(android.R.id.content);
fragTransaction.add(root_view.getId(), cameraFragment, "Camera Preview");
fragTransaction.commit();
}
Static variables in Java are kept across Activity creation/destruction - they are associated with the class itself but not a particular instance of the class.
See the official documentation here:
http://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html
Your application doesn't end when the user returns to the home screen, it just gets put in a background state. If you force stopped the application and restarted it, then the static FragmentManager will be null.
With regards to CameraFragment, unless you've set setRetainInstance(true), it will get destroyed on an orientation change.
==== EDIT
Here's a more detailed flow of what's happening...
You open the application up for the first time
Activity, say instance A1, gets created and its corresponding FragmentManager instance, FM1, also gets created
You store FM1 as a static variable
You go back to home
Activity A1 and FM1 gets destroyed because of the normal Activity lifecycle, although FM1's reference is still held onto by the static variable. At this point, FM1 loses all the fragments it contains and isDestroyed() will return true.
Starting the app again
New Activity instance A2 gets created along with its new FragmentManager instance FM2
Related
As an alternative to an Intent, i'm saving data in a retained headless Fragment during Activity re-creation (my saved object can be pretty large and it wouldn't fit the size limit of an Intent, and i think this is a faster approach than serializing-deserializing into JSON for example).
I've got the idea from this Google documentation, although my implementation is a bit different.
The Fragment:
public class DataFragment extends Fragment {
private Data data;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
public void setData(Data data) {
this.data = data;
}
public Data getData() {
return data;
}
}
I save my data to this Fragment in the onSaveInstanceState() method of my Activity:
#Override
protected void onSaveInstanceState(Bundle outState) {
FragmentManager fm = getSupportFragmentManager();
dataFragment = (DataFragment) fm.findFragmentByTag(TAG_DATA);
if (dataFragment == null) {
dataFragment = new DataFragment();
fm.beginTransaction().add(dataFragment, TAG_DATA).commitNow();
}
dataFragment.setData(myData);
super.onSaveInstanceState(outState);
}
And the relevant part of onCreate():
Data data;
FragmentManager fm = getSupportFragmentManager();
dataFragment = (DataFragment) fm.findFragmentByTag(TAG_DATA);
if (dataFragment == null) {
// the Fragment is not attached, fetching data from DB
DatabaseManager dbm = DatabaseManager.getInstance(this);
data = dbm.getData();
} else {
// the Fragment is attached, fetching the data from it
data = dataFragment.getData();
fm.beginTransaction().remove(dataFragment).commitNow();
}
This works flawlessly on orientation changes.
The problem is, sometimes, when my app is in the background and i'm returning to it, dataFragment.getData() returns null.
In other words, in the following line in onCreate() sometimes data is null:
data = dataFragment.getData();
How is this possible?
It does not throw a NullPointerException, so dataFragment is not null for sure.
Why did its initialized instance variable became null?
What you experience is PROCESS DEATH.
Technically it's also called "low memory condition".
The retained fragment is killed along with the application, but the FragmentActivity recreates your retained fragment in super.onCreate(), so you'll find it by its tag but the data in it won't be initialized.
Put the app in background then press the red X in the bottom left in Android Studio to kill the process. That recreates this phenomenon.
NOTE: After AS 4.0, if you launch your app from AS, then "Terminate" will trigger Force Stop (which does not produce this phenomenon). But if you launch your app from LAUNCHER on the phone after that, then you'll get this phenomenon.
if activity is recreated after it was previously destroyed, you are able to saved your state from the Bundle that the system passes your activity. Both the onCreate() and onRestoreInstanceState() callback methods receive the same Bundle that contains the instance state information.
Obviously yours onCreate() method is called whether the system is creating a new instance of your activity or recreating a previous one, you need to check whether the state Bundle is null before you attempt to read it. If it is null, then the system is creating a new instance of the activity, instead of restoring a previous one that was destroyed.
in oncreate():
Data data;
FragmentManager fm = getSupportFragmentManager();
dataFragment = (DataFragment) fm.findFragmentByTag(TAG_DATA);
**if (dataFragment.getData()== null) {**
// the Fragment is not attached, fetching data from DB
DatabaseManager dbm = DatabaseManager.getInstance(this);
data = dbm.getData();
} else {
// the Fragment is attached, fetching the data from it
data = dataFragment.getData();
fm.beginTransaction().remove(dataFragment).commitNow();
}
My code:
public class MainActivity extends AppCompatActivity {
private FragmentA fragmentA;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
fragmentA = FragmentA.newInstance();
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.fragment_a_container, fragmentA, "FRAGMENT_A");
fragmentTransaction.commit();
}
else {
fragmentA = (FragmentA) getSupportFragmentManager().findFragmentByTag("FRAGMENT_A");
}
}
}
I don't really know what I am doing but this is currently what I do. I define a container for the Fragment and then I use a FragmentTransaction to replace it with a Fragment. The part I am confused about though is the else statement.
Should I be structuring this differently?
I thought configuration changes wiped out Activities and Fragments so why check for the Fragment in some support manager? Does this mean Fragments don't actually get destroyed? At the same time, they DO seem to get destroyed because they appear to reset unless I use onSaveInstanceState or the getArguments() approach.
Edit: What's wrong with doing this:
public class MainActivity extends AppCompatActivity {
private FragmentA fragmentA;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
fragmentA = FragmentA.newInstance();
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.fragment_a_container, fragmentA, "FRAGMENT_A");
fragmentTransaction.commit();
}
}
They do get destroyed and recreated for you on configuration changes by the, in this case, SupportFragmentManager.
To answer your questions:
Should I be structuring this differently?
No, that's exactly how you should create fragments if there is no saved state and retrieve them when there is. See also my answer here;
a) so why check for the Fragment in some support manager?
Because the manager handles the lifecyle of the fragment for you when there is a configuration change.
b) Does this mean Fragments don't actually get destroyed?
No, it does get destroyed. See this diagram for a reference.
Edit to answer some of your questions from the comments:
But any member variables inside that Fragment are completely lost on configuration change unless I save them in that Fragment's onSaveInstanceState, right?
That is correct. Because your fragment is being destroyed, everything not being saved on onSaveInstanceState gets lost.
So then what exactly am I restoring?
You are not restoring anything. You are only retrieving the reference to the fragment that was previously created. You restore your variables on the onRestoreInstanceState() method of your fragment.
What's wrong with doing this (the code from the edit in the question)?
If you do that, you are adding a new fragment instance to the R.id.fragment_a_container container. So the old fragment will get lost together with the state of it you saved on onSaveInstanceState(). It will be a new fragment, with new information in it and the event onRestoreInstanceState() won't be called for it.
I've got an activity with a retained fragment. This fragment handles DB queries and sends the results back to the activity via an interface.
When I rotate the device, the activity is destroyed and re-creates itself as expected. It also re-connects to the retained fragment (which wasn't destroyed) which is continuing to handle the DB queries despite the device rotation.
My problem is when the retained fragment gets the DB queries result back, it tries to send these via the interface to the activity. But, if the device has been rotated, the activity could be in the destroyed state (and not yet re-created) so the fragment can't send the results to the activity.
When the activity is eventually re-created, it's missed "it's chance" to receive the interface calls from the fragment and get the DB results. How to solve this?
Just a bit more detail - the activity has a button which the user presses to start the retained fragment doing the DB queries (they don't just start automatically when the activity is created or the activity attached to the fragment).
Activity.onCreate()
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity);
// Get our Retained Fragment if already exists, otherwise create a new one
FragmentManager fragManager = getSupportFragmentManager();
mRetFrag = (QueryFragment) fragManager.findFragmentByTag(QueryFragment.FRAGMENT_TAG);
if (mRetFrag == null) {
// First time - create a new retained fragment
mRetFrag = QueryFragment.newInstance();
fragManager.beginTransaction().add(mRetFrag, QueryFragment.FRAGMENT_TAG).commit();
}
// Button to start DB querying in fragment
Button queryBtn = (Button) findViewById(R.id.query_button);
queryBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mRetFrag.runDBQueries();
}
});
}
And in the fragment when it has the DB query results, I try to send this back to the Activity. So, mCallbackListener could be null if the activity isn't yet created.
// Pass the data to the activity
if (mCallbackListener != null) {
mCallbackListener.onQueryFinished(data);
}
Your mCallbackListener points to an activity, which doesn't exist anymore, as long as it has been destroyed after rotation.
You have to get another instance of your activity:
if(null == mCallbackListener) {
mCallbackListener = (MainActivity) getActivity();
}
Another solution is to use event bus like Greenrobot's EventBus or Otto.
Here's nice blog about how to use Otto.
"If an activity is paused or stopped, the system can drop the activity from memory by either asking it to finish, or simply killing its process.". When the user goes back to the activity, it restores it's state with a bundle.
My question is:
is it important to do this in oncreate:
if(savedinstance != null)
{
fragement = fm.findFragmentByTag("tag");
}
else
{
fragment = new Fragment();
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.webViewFrame,fragment,"tag");
ft.commit()
}
instead of just this:
fragment = new Fragment();
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.webViewFrame,fragment,"tag");
ft.commit()
If you are correctly saving the state of your activity and fragment in onSaveInstanceState(), and you want your activity to be recreated in the state it had before it was killed, you should use the first block of code you posted.
The FragmentManager saves its state, and when recreated, restores the fragments it was holding when the activity was killed. It steps them through the build-up lifecycle events using the fragment's saved state: create, createView, start, resume.
I'm pretty sure if you try running the second block of code, you will find after restart that you have two instances of your fragment in the FragmentManager--the one added when the activity was first create, and the one added after restart.
For this all to work correctly, you must carefully save the state of both your activity and fragment(s) in the onSaveInstanceState() method of each, and then in onCreate() test savedInstanceState and when not null, use the bundle to restore the state of your activity/fragment.
This is the guide for saving/restoring activity state. More info here.
In the fragment doc, in one of the example, they check for savedInstanceState == null when adding a fragment:
public static class DetailsActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getResources().getConfiguration().orientation
== Configuration.ORIENTATION_LANDSCAPE) {
// If the screen is now in landscape mode, we can show the
// dialog in-line with the list so we don't need this activity.
finish();
return;
}
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();
}
}
}
What is the purpose of this check? What would happen if it is not there?
What is the purpose of this check?
To not add the fragment twice, though I prefer checking to see if the fragment is there instead of relying on that Bundle being null.
What would happen if it is not there?
Initially, nothing, as the Bundle will be null when the activity is first created.
However, then, the user rotates the device's screen from portrait to landscape. Or, the user changes languages. Or, the user puts the device into a manufacturer-supplied car dock. Or, the user does any other configuration change.
Your activity will be destroyed and recreated by default. Your fragments will also be destroyed and recreated by default (exception: those on which setRetainInstance(true) are called, which are detached from the old activity and attached to the new one).
So, the second time the activity is created -- the instance created as a result of the configuration change -- your fragment already exists, as it was either recreated or retained. You don't want a second instance of that fragment (usually), and therefore you take steps to detect that this has occurred and not run a fresh FragmentTransaction.