On fragments and life-cycles
“When should we trigger dependency injection in fragments?” Was a question my colleague asked me.
“Well why not just in Fragment#onCreate()
?”, I replied. However, my colleague pointed out that apparantly Fragment#onCreate()
is
not always called at the same time with relation to Activity#onCreate()
. This is important for us as we
initialize a Dagger component in the Activity and use that in the Fragment.
We could of course just blindly follow the advise on the internet and solve this problem by doing
whatever we want to do in Fragment#onActivityCreated()
, but I wanted to know why the life-cycle methods where not always called
in the same order.
For our purposes let’s consider the following piece of code:
public class MyActivity extends FragmentActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.my_layout);
if (savedInstanceState == null) {
Fragment f = new MyFragment();
FragmentTransaction t = getFragmentManager().beginTransaction();
t.add(R.id.fragment_container, f);
t.commit();
}
}
}
We will analyze what happens when we first launch MyActivity
and what happens later when it gets re-created. We will look
at when the life-cycle methods are called and why they are called at that specific time. Hopefully this will give you a better
understanding of the inner workings of fragments.
First launch
When we launch our activity for the first time, our UI will not contain any fragments yet, so we will add them ourselves.
We achieve this by checking if savedInstanceState == null
, if true
this is a fresh launch of MyActivity
.
We start by constructing a new instance of MyFragment
. This will not call any life-cycle methods on the fragment, it only
calls the normal constructor. On the next line we begin
our FragmentTransaction
. As this does not touch our fragment no life-cycle methods are called. We follow by adding the fragment
instance to the transaction. This specifies that the fragment should be added to the FragmentManager
and to the UI as a child
of R.id.fragment_container
, but also does not call any life-cycle methods on the fragment. Finally, we commit the transaction,
which again will also not call any life-cycle methods on your fragment… Wait, what??? If none of those lines trigger the life-cycle
methods, then when do they get called?
FragmentTransaction#commit();
If you look at the documentation of FragmentTransaction#commit() we already get a hint of what is going on. It reads “Schedules a commit of this transaction. The commit does not happen immediately; it will be scheduled as work on the main thread to be done the next time that thread is ready.”
First let’s have a look at what FragmentTransaction#commit()
actually does. As FragmentTransaction
is abstract we will be looking
at BackStackRecord#commit()
, which is the actual implementation used in the support lib:
final class BackStackRecord extends FragmentTransaction implements
FragmentManager.BackStackEntry, Runnable {
// ...
public int commit() {
return commitInternal(false);
}
public int commitAllowingStateLoss() {
return commitInternal(true);
}
int commitInternal(boolean allowStateLoss) {
if (mCommitted) throw new IllegalStateException("commit already called");
if (FragmentManagerImpl.DEBUG) {
Log.v(TAG, "Commit: " + this);
LogWriter logw = new LogWriter(TAG);
PrintWriter pw = new PrintWriter(logw);
dump(" ", null, pw, null);
}
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);
} else {
mIndex = -1;
}
mManager.enqueueAction(this, allowStateLoss);
return mIndex;
}
// ...
}
The crucial line here is mManager.enqueueAction(this, allowStateLoss);
. mManager
is the FragmentManager
, which in turn will
call mActivity.mHandler.post()
with this BackStackRecord
instance (as it implements Runnable
). So, everything in
BackStackRecord#run()
will be executed during the next frame, not the current one. You can read for yourself what
BackStackRecord#run() actually does here, but in a nutshell it adds/removes/replaces/etc the fragment to the FragmentManager
and then calls FragmentManagerImpl#moveToState(Fragment, int, int, int, boolean) which is where the actual life-cycle
methods are called from your fragment.
Although you call commit in MyActivity#onCreate(Bundle)
, you can see that the fragment’s life-cycle methods are not actually
called until the next UI frame iteration. At that point all your activity life-cycle methods (onCreate
, onStart
and onResume
) have already completed.
Instance re-creation
We have seen in which order the life-cycle methods of a Fragment
and Activity
are called if the Fragment
is added to the UI in MyActivity#onCreate(Bundle)
by means of a FragmentTransaction
, but what
happens to the order if MyActivity
gets destroyed and re-created, for instance when we rotate the screen?
As hinted by all the questions on StackOverflow, the calling of life-cycle methods during recreation happens
at a different point in time than when the fragment is initially added. To be
specific, it happens during FragmentActivity#onCreate(Bundle)
which we call in the first line of our MyActivity#onCreate(Bundle)
(super.onCreate(savedInstanceState);
).
Let’s have a look at what is going on here:
public class FragmentActivity extends Activity {
// ...
/**
* Perform initialization of all fragments and loaders.
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
mFragments.attachActivity(this, mContainer, null);
// Old versions of the platform didn't do this!
if (getLayoutInflater().getFactory() == null) {
getLayoutInflater().setFactory(this);
}
super.onCreate(savedInstanceState);
NonConfigurationInstances nc = (NonConfigurationInstances)
getLastNonConfigurationInstance();
if (nc != null) {
mAllLoaderManagers = nc.loaders;
}
if (savedInstanceState != null) {
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
}
mFragments.dispatchCreate();
}
// ...
}
If the savedInstanceState != null
, then FragmentManagerImpl#restoreAllState(Parcelable, NonConfigurationInstances)
is called,
is where all the fragment instances are re-created. On the last line FragmentManagerImpl#dispatchCreate();
is called.
This last line will actually call Fragment#onCreate(Bundle)
on your fragment instance, which means that Fragment#onCreate(Bundle)
will be called during your call to FragmentActivity#onCreate(Bundle)
in your activity. (Note that all
life-cycle methods of your fragments will now be called during their counterparts of FragmentActivity
.)
As explained above the life-cycle methods of your Fragment
are actually invoked during their counterparts of the Activity
,
which means that if your Fragment
depends on certain objects created or set during Activity#onCreate(Bundle)
,
you will need to create or set these objects before your call to super.onCreate(savedInstanceState)
or (as suggested in the above SO link)
move the logic in the fragment to a different life-cycle method (like Fragment#onActivityCreated()
, which is guaranteed to be
called after Activity#onCreate()
.
Bonus: Fragments in layouts?
What about if you add your Fragment
in your layout instead? When will the life-cycle methods of your Fragment
then be called with
respect to those of the Activity
?
Unsurprisingly the creation of the fragments in layouts is kicked off by FragmentActivity#setContentView(int)
as the
fragments are declared in your layout file. The creation of the fragments is done by FragmentActivity
and happens because
FragmentActivity
intercepts the fragment
tags by overriding the Factory2
implementation of Activity
.
Let’s see what happens in FragmentActivity
implementation of Factory2
:
public class FragmentActivity extends Activity {
// ...
public View onCreateView(String name, @NonNull Context context, @NonNull AttributeSet attrs) {
if (!"fragment".equals(name)) {
return super.onCreateView(name, context, attrs);
}
final View v = mFragments.onCreateView(name, context, attrs);
if (v == null) {
return super.onCreateView(name, context, attrs);
}
return v;
}
// ...
}
The above shows that if the tag is a fragment
tag, the FragmentActivity
will delegate the creation of the view to the FragmentManagerImpl
.
In FragmentManagerImpl#onCreateView(View, String, Context, AttributeSet) the fragment class is instantiated and
the fragment is moved to the Fragment.CREATED
state. With fragments in layouts, this state also creates the view.
Wrap up
As you can see, using fragments in layouts will make sure the initial calls to the fragments life-cycle methods will be in the same order with relation to the activity life-cycle methods when the fragment is recreated during activity recreation. If your activity just has one (or more) static fragments that you don’t replace or remove, then it might make sense to add these fragments in a layout file instead of a fragment transaction as it will simplify dealing with activity recreation.
If you are dynamically adding, removing or replacing fragments, then be aware of the order of the life-cycle methods as your fragments
life-cycle methods will be called before the activity counterparts have fully completed. For our purposes we chose to intialize our
Dagger component before the call to super.onCreate(Bundle)
in our Activity.
Please note that if you run into trouble with this, it might indicate that you have a dependency to the activity which creates coupling with your activity. You might want to consider if a fragment is the best solution here and if so whether it is possible to remove the coupling with your activity. After all, the purpose of fragments is to have re-usable pieces of UI, but if the fragments have strong dependencies on the order of things happening in the activity, they might not turn out to be that re-usable at all.