Background:
I am writing an android application with an activity that can be populated by one of a number of fragments. Relatively often, I need to pass a bundle of data to the active fragment so that it can update the UI accordingly.
To get the current fragment, I call getfragmentManager().findFragmentByTag()
and cast the returned fragment to my custom fragment class.
The problem:
The above method usually works fine. However, findFragmentByTag()
occasionally returns null. Upon further investigation, I have concluded that this will only occur when I run or debug from Android studio (and even then it doesn't happen every time).
Relevant Code:
protected void onCreate(Bundle savedInstanceState){
//Do lots of stuff
currentFragmentTag = "";
getFragmentManager().addOnBackStackChangedListener(
new FragmentManager.OnBackStackChangedListener() {
public void onBackStackChanged() {
if (getFragmentManager().getBackStackEntryCount() > 0) {
currentFragmentTag = getFragmentManager().getBackStackEntryAt(getFragmentManager().getBackStackEntryCount() - 1).getName();
}
}
});
init();
//Do some more stuff
//this should always be the case
if (currentFragmentTag.equals(LOGIN_FRAGMENT_TAG)) {
((LoginFragment) getFragmentManager().findFragmentByTag(currentFragmentTag)).beginLogin();
}
}
private void init(){
//changed to reflect George Mulligan's advice
Fragment currentFrag = getFragmentManager().findFragmentByTag(LOGIN_FRAGMENT_TAG);
if(currentFrag == null) {
currentFragmentTag = LOGIN_FRAGMENT_TAG;
getFragmentManager().beginTransaction()
.add(R.id.container, loginFrag, LOGIN_FRAGMENT_TAG)
.addToBackStack(LOGIN_FRAGMENT_TAG)
.commit();
getFragmentManager().executePendingTransactions();
}
}
public void updateFragment(){
Bundle dataBundle = new Bundle();
//put stuff in dataBundle
if (currentFragmentTag.equals(LOGIN_FRAGMENT_TAG)) {
LoginFragment currentFrag = (LoginFragment) getFragmentManager().findFragmentByTag(currentFragmentTag);
if (currentFrag != null) {
currentFrag.passBundleToFragment(dataBundle);
Log.d(TAG, "Fragment returned is valid.");
} else {
Log.d(TAG, "Fragment returned is null.");
}
}
//else if a different fragment is active then update it in the same way
}
//Manually open the loginFragment. This can be called from other fragments. My problem always occurs before this is called however.
@Override
public void openLoginScreen() {
if(/*some conditions*/) {
LoginFragment loginFrag = LoginFragment.newInstance();
getFragmentManager().beginTransaction()
.replace(R.id.container, loginFrag, LOGIN_FRAGMENT_TAG)
.addToBackStack(LOGIN_FRAGMENT_TAG)
.commit();
getFragmentManager().executePendingTransactions();
currentFragmentTag = LOGIN_FRAGMENT_TAG;
updateFragment();
}
}
Normally, my logcat looks something like this:
Fragment returned is valid
Fragment returned is valid
Fragment returned is valid
Fragment returned is valid
Fragment returned is valid
Fragment returned is valid
Fragment returned is valid
Fragment returned is valid
...etc.
But every now and then, and only when I start the app from Android Studio, I get something like:
Fragment returned is valid
Fragment returned is valid
Fragment returned is valid
Fragment returned is valid
Fragment returned is null
Fragment returned is null
Fragment returned is null
Fragment returned is null
Fragment returned is null
Fragment returned is null
Fragment returned is null
Fragment returned is null
Fragment returned is null
Fragment returned is null
Fragment returned is valid
Fragment returned is valid
Fragment returned is valid
Fragment returned is valid
Fragment returned is valid
Fragment returned is valid
Fragment returned is valid
Fragment returned is valid
Fragment returned is valid
Fragment returned is null
Fragment returned is null
Fragment returned is null
Fragment returned is null
Fragment returned is null
Fragment returned is null
Fragment returned is null
Fragment returned is valid
Fragment returned is valid
What on Earth is going on here?
UPDATE:
I have been able to reproduce this error while disconnected from Android Studio by clicking the app switch button, closing my app and immediately restarting it. Provided I do this quickly enough, it never fails to behave as I described above.
After some more logging and chasing down other issues, I discovered that in these particular cases, onCreate()
is being called twice.
My app is designed to run only in landscape mode, in part to avoid issues that come with recreating the activity. It would seem, however, that when the app is closed and restarted quickly, Android never finishes the necessary rotation to portrait mode for the home screen before my app is launched again. My assumption is that this causes the OS to rotate back to landscape after the app is running and thereby restart it.
All of this is fine and dandy, except for the fact that it doesn't explain why findFragmentByTag()
sometimes returns null.
Every object in my Activity class should be recreated, right? So shouldn't the FragmentManager be re-initialized as well? Or is getFragmentManager()
a static reference to something outside of the Activity itself?
SECOND UPDATE:
I tried George's idea of checking if the fragment had already been added before I call beginTransaction()
and, although it didn't solve the problem, I noticed something strange when debugging:
I set a breakpoint at Log.d(TAG, "Fragment returned is null.");
. Closing the app and quickly restarting it guarantees that this code will be reached as I mentioned above. Then, if I view the Fragment Manager by calling getFragmentManager()
in the Evaluate Expression window, I notice that a `Login Fragment' has already been added, but it doesn't have a tag associated with it.
Setting a breakpoint at Log.d(TAG, "Fragment returned is valid.");
in the same app session, however, reveals the LoginFragment
is added with a tag as would be expected.
There is no point in my code where I ever add a fragment without setting a tag. Could this have something to do with the activity being recreated and the Fragment manager losing tags even though it holds onto the fragments themselves?
A few things to note on this.
In updateFragment
you should really compare your Strings using .equals
instead of reference comparison ==
so LOGIN_FRAGMENT_TAG.equals(currentFragmentTag)
as a best practice and to avoid confusion.
Also, the FragmentManager
will remember which fragments you have added to it across orientation changes. So in the one example you mentioned where you get into onCreate
twice you will then likely have two instances of your LoginFrag
on the back stack.
You can avoid this by doing the same lookup you are doing in your updateFragment
method and only adding the fragment if it isn't found.
LoginFragment currentFrag = (LoginFragment) getFragmentManager()
.findFragmentByTag(LOGIN_FRAGMENT_TAG);
if(currentFrag == null) {
currentFrag = getFragmentManager().beginTransaction()
.replace(R.id.container, loginFrag, LOGIN_FRAGMENT_TAG)
.addToBackStack(LOGIN_FRAGMENT_TAG)
.commit();
//executePendingTransactions() usually isn't necessary...
getFragmentManager().executePendingTransactions();
}
I'm guessing multiple fragments are used in this activity since you bother having the String currentFragmentTag
but if not then it would be better for you to just keep a reference to your LoginFragment
as a field on the class so you can avoid the additional lookup of it in the updateFragment
method.
Other than that I tried reproducing your error and I cannot so the error might be elsewhere in code you are not showing.
This isn't a direct answer to your question, but a suggestion for avoiding the problem in the first place.
Because the interaction between the Fragment and Activity Lifecycles is so complex and difficult to reason about, I've found that it's much easier if you completely decouple the two.
Instead of obtaining a reference to the Fragment and invoking a method on it directly, I set up a message bus (e.g. Otto or GreenRobot) and in your case, would have the Activity post a message to the bus, and have the Fragment subscribe to that message. Much cleaner, and if the Fragment happens to not be ready (hasn't registered with the bus yet), there's no error condition.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With